From a239ba6d2bbad7f07c8d84d1449cd40c8a72f97c Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Thu, 24 Oct 2024 18:29:55 +0800 Subject: [PATCH 001/192] Set up collab service --- backend/collab-service/.env.sample | 4 + backend/collab-service/Dockerfile | 13 + backend/collab-service/app.ts | 30 + backend/collab-service/eslint.config.js | 10 + backend/collab-service/package-lock.json | 6697 +++++++++++++++++ backend/collab-service/package.json | 38 + backend/collab-service/server.ts | 9 + .../src/controllers/collabController.ts | 0 .../collab-service/src/routes/collabRoutes.ts | 5 + backend/collab-service/swagger.yml | 23 + backend/question-service/swagger.yml | 2 +- docker-compose.yml | 17 + 12 files changed, 6847 insertions(+), 1 deletion(-) create mode 100644 backend/collab-service/.env.sample create mode 100644 backend/collab-service/Dockerfile create mode 100644 backend/collab-service/app.ts create mode 100644 backend/collab-service/eslint.config.js create mode 100644 backend/collab-service/package-lock.json create mode 100644 backend/collab-service/package.json create mode 100644 backend/collab-service/server.ts create mode 100644 backend/collab-service/src/controllers/collabController.ts create mode 100644 backend/collab-service/src/routes/collabRoutes.ts create mode 100644 backend/collab-service/swagger.yml diff --git a/backend/collab-service/.env.sample b/backend/collab-service/.env.sample new file mode 100644 index 0000000000..1800f50753 --- /dev/null +++ b/backend/collab-service/.env.sample @@ -0,0 +1,4 @@ +NODE_ENV=development +SERVICE_PORT=3003 + +ORIGINS=http://localhost:5173,http://127.0.0.1:5173 \ No newline at end of file diff --git a/backend/collab-service/Dockerfile b/backend/collab-service/Dockerfile new file mode 100644 index 0000000000..9f34638d16 --- /dev/null +++ b/backend/collab-service/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /collab-service + +COPY package*.json . + +RUN npm ci + +COPY . . + +EXPOSE 3003 + +CMD ["npm", "run", "dev"] diff --git a/backend/collab-service/app.ts b/backend/collab-service/app.ts new file mode 100644 index 0000000000..ba884aa2f3 --- /dev/null +++ b/backend/collab-service/app.ts @@ -0,0 +1,30 @@ +import express, { Request, Response } from "express"; +import dotenv from "dotenv"; +import fs from "fs"; +import yaml from "yaml"; +import swaggerUi from "swagger-ui-express"; + +import collabRoutes from "./src/routes/collabRoutes.ts"; + +dotenv.config(); + +const allowedOrigins = process.env.ORIGINS + ? process.env.ORIGINS.split(",") + : ["http://localhost:5173", "http://127.0.0.1:5173"]; + +const file = fs.readFileSync("./swagger.yml", "utf-8"); +const swaggerDocument = yaml.parse(file); + +const app = express(); + +app.use(express.json()); + +app.use("/api/collab", collabRoutes); + +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + +app.get("/", (req: Request, res: Response) => { + res.status(200).json({ message: "Hello world from collab service" }); +}); + +export default app; diff --git a/backend/collab-service/eslint.config.js b/backend/collab-service/eslint.config.js new file mode 100644 index 0000000000..3c8af371cd --- /dev/null +++ b/backend/collab-service/eslint.config.js @@ -0,0 +1,10 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + { files: ["**/*.{js,mjs,cjs,ts}"] }, + { languageOptions: { globals: globals.node } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; diff --git a/backend/collab-service/package-lock.json b/backend/collab-service/package-lock.json new file mode 100644 index 0000000000..6a4b1388a1 --- /dev/null +++ b/backend/collab-service/package-lock.json @@ -0,0 +1,6697 @@ +{ + "name": "collab-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "collab-service", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.7.7", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.6.0" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.7.9", + "@types/swagger-ui-express": "^4.1.6", + "cross-env": "^7.0.3", + "eslint": "^9.13.0", + "globals": "^15.11.0", + "jest": "^29.7.0", + "tsx": "^4.19.1", + "typescript": "^5.6.3", + "typescript-eslint": "^8.11.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.9.tgz", + "integrity": "sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.25.9", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.9.tgz", + "integrity": "sha512-yD+hEuJ/+wAJ4Ox2/rpNv5HIuPG82x3ZlQvYVn8iYCprdxzE7P1udpGF1jyjQVBU4dgznN+k2h103vxZ7NdPyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.9.tgz", + "integrity": "sha512-WYvQviPw+Qyib0v92AwNIrdLISTp7RfDkM7bPqBvpbnhY4wq8HvHBZREVdYDXk98C8BkOIVnHAY3yvj7AVISxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helpers": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "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/@babel/generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.9.tgz", + "integrity": "sha512-omlUGkr5EaoIJrhLf9CJ0TvjBRpd9+AXRG//0GEQ9THSo8wPiTlbpy1/Ow8ZTrbXpjd9FHXfbFQx32I04ht0FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.9.tgz", + "integrity": "sha512-TvLZY/F3+GvdRYFZFyxMvnsKi+4oJdgZzU3BoGN9Uc2d9C6zfNwJcKKhjqLAhK8i46mv93jsO74fDh3ih6rpHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-simple-access": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", + "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.9.tgz", + "integrity": "sha512-oKWp3+usOJSzDZOucZUAMayhPz/xVjzymyDzUN8dk0Wd3RWMlGLXi07UCQ/CgQVb8LvXx3XBajJH4XGgkt7H7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", + "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.9.tgz", + "integrity": "sha512-u3EN9ub8LyYvgTnrgp8gboElouayiwPdnM7x5tcnW3iSt09/lQYPwMNK40I9IUxo7QOZhAsPHCmmuO7EPdruqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "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/@babel/types": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", + "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "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/@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "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" + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "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/@eslint/js": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz", + "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "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", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", + "integrity": "sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/type-utils": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz", + "integrity": "sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "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/@typescript-eslint/scope-manager": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", + "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.11.0.tgz", + "integrity": "sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "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/@typescript-eslint/types": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", + "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", + "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "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/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", + "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz", + "integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "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" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.45", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.45.tgz", + "integrity": "sha512-vOzZS6uZwhhbkZbcRyiy99Wg+pYFV5hk+5YaECvx0+Z31NR3Tt5zS6dze2OepT6PCTzVzT0dIJItti+uAW5zmw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", + "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.13.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/ms": { + "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/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "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/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-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" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "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/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tsx": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz", + "integrity": "sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.11.0.tgz", + "integrity": "sha512-cBRGnW3FSlxaYwU8KfAewxFK5uzeOAp0l2KebIlPDOT5olVi65KDG/yjBooPBG0kGW/HLkoz1c/iuBFehcS3IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.11.0", + "@typescript-eslint/parser": "8.11.0", + "@typescript-eslint/utils": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.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/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json new file mode 100644 index 0000000000..552a3b9808 --- /dev/null +++ b/backend/collab-service/package.json @@ -0,0 +1,38 @@ +{ + "name": "collab-service", + "version": "1.0.0", + "main": "server.ts", + "scripts": { + "start": "tsx server.ts", + "dev": "tsx watch server.ts", + "test": "cross-env NODE_ENV=test && jest", + "lint": "eslint ." + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.7.7", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.6.0" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.7.9", + "@types/swagger-ui-express": "^4.1.6", + "cross-env": "^7.0.3", + "eslint": "^9.13.0", + "globals": "^15.11.0", + "jest": "^29.7.0", + "tsx": "^4.19.1", + "typescript": "^5.6.3", + "typescript-eslint": "^8.11.0" + } +} diff --git a/backend/collab-service/server.ts b/backend/collab-service/server.ts new file mode 100644 index 0000000000..34443b0614 --- /dev/null +++ b/backend/collab-service/server.ts @@ -0,0 +1,9 @@ +import app from "./app"; + +const PORT = process.env.SERVICE_PORT || 3003; + +if (process.env.NODE_ENV !== "test") { + app.listen(PORT, () => { + console.log(`Collab service server listening on http://localhost:${PORT}`); + }); +} diff --git a/backend/collab-service/src/controllers/collabController.ts b/backend/collab-service/src/controllers/collabController.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/collab-service/src/routes/collabRoutes.ts b/backend/collab-service/src/routes/collabRoutes.ts new file mode 100644 index 0000000000..9da7196e6e --- /dev/null +++ b/backend/collab-service/src/routes/collabRoutes.ts @@ -0,0 +1,5 @@ +import express from "express"; + +const router = express.Router(); + +export default router; diff --git a/backend/collab-service/swagger.yml b/backend/collab-service/swagger.yml new file mode 100644 index 0000000000..5f8bc152eb --- /dev/null +++ b/backend/collab-service/swagger.yml @@ -0,0 +1,23 @@ +openapi: 3.0.0 + +info: + title: Collab Service + version: 1.0.0 + +paths: + /: + get: + tags: + - root + summary: Root + description: Ping the server + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string diff --git a/backend/question-service/swagger.yml b/backend/question-service/swagger.yml index 9032569b0b..249b6ea0b4 100644 --- a/backend/question-service/swagger.yml +++ b/backend/question-service/swagger.yml @@ -353,7 +353,7 @@ paths: /api/questions/images: post: summary: Publish image to firebase storage - tags: + tags: - questions security: - bearerAuth: [] diff --git a/docker-compose.yml b/docker-compose.yml index 003a03c395..629a715847 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,6 +55,21 @@ services: - /matching-service/node_modules restart: on-failure + collab-service: + image: peerprep/collab-service + build: ./backend/collab-service + environment: + - CHOKIDAR_USEPOLLING=true + env_file: ./backend/collab-service/.env + ports: + - 3003:3003 + networks: + - peerprep-network + volumes: + - ./backend/collab-service:/collab-service + - /collab-service/node_modules + restart: on-failure + frontend: image: peerprep/frontend build: ./frontend @@ -65,6 +80,8 @@ services: depends_on: - user-service - question-service + - matching-service + - collab-service networks: - peerprep-network volumes: From 87a12884e7fd48c13c1700293c4ab7493c61df6c Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Thu, 24 Oct 2024 19:21:22 +0800 Subject: [PATCH 002/192] Add tsconfig --- backend/collab-service/tsconfig.json | 110 +++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 backend/collab-service/tsconfig.json diff --git a/backend/collab-service/tsconfig.json b/backend/collab-service/tsconfig.json new file mode 100644 index 0000000000..830b218c6e --- /dev/null +++ b/backend/collab-service/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "noEmit": true /* Disable emitting files from a compilation. */, + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} From 51e85278a859c79215946b5273147a06694d3849 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Thu, 24 Oct 2024 19:26:16 +0800 Subject: [PATCH 003/192] Add cors --- backend/collab-service/app.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/collab-service/app.ts b/backend/collab-service/app.ts index ba884aa2f3..3b35ab1247 100644 --- a/backend/collab-service/app.ts +++ b/backend/collab-service/app.ts @@ -3,6 +3,7 @@ import dotenv from "dotenv"; import fs from "fs"; import yaml from "yaml"; import swaggerUi from "swagger-ui-express"; +import cors from "cors"; import collabRoutes from "./src/routes/collabRoutes.ts"; @@ -17,6 +18,10 @@ const swaggerDocument = yaml.parse(file); const app = express(); +app.use(cors({ origin: allowedOrigins, credentials: true })); + +app.options("*", cors({ origin: allowedOrigins, credentials: true })); + app.use(express.json()); app.use("/api/collab", collabRoutes); From 11b5b93799b28a71e76318c0ae7982421eb92165 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Thu, 24 Oct 2024 22:33:37 +0800 Subject: [PATCH 004/192] Add dockerignore --- backend/collab-service/.dockerignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 backend/collab-service/.dockerignore diff --git a/backend/collab-service/.dockerignore b/backend/collab-service/.dockerignore new file mode 100644 index 0000000000..4abc77f632 --- /dev/null +++ b/backend/collab-service/.dockerignore @@ -0,0 +1,5 @@ +coverage +node_modules +tests +.env* +*.md From ffd7b27b1accc60773f8be6817e2e7be4cbee8ed Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Fri, 25 Oct 2024 09:59:43 +0800 Subject: [PATCH 005/192] Add random question endpoint --- .../src/controllers/questionController.ts | 37 ++++++++++++---- .../src/routes/questionRoutes.ts | 3 ++ backend/question-service/src/utils/types.ts | 12 +++++ backend/question-service/swagger.yml | 44 +++++++++++++++++++ 4 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 backend/question-service/src/utils/types.ts diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 1d01443cbc..a6e0694aff 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -19,6 +19,7 @@ import { import { upload } from "../../config/multer"; import { uploadFileToFirebase } from "../utils/utils"; +import { QnListSearchFilterParams, RandomQnCriteria } from "../utils/types.ts"; export const createQuestion = async ( req: Request, @@ -157,14 +158,6 @@ export const deleteQuestion = async ( } }; -interface QnListSearchFilterParams { - page: string; - qnLimit: string; - title?: string; - complexities?: string | string[]; - categories?: string | string[]; -} - export const readQuestionsList = async ( req: Request, res: Response, @@ -246,6 +239,34 @@ export const readQuestionIndiv = async ( } }; +export const readRandomQuestion = async ( + req: Request, + res: Response, +): Promise => { + try { + const { category, complexity } = req.query; + + const randomQuestion = await Question.aggregate([ + { + $match: { $and: [{ category }, { complexity: { $in: [complexity] } }] }, + }, + { $sample: { size: 1 } }, + ]); + + if (!randomQuestion) { + res.status(404).json({ message: QN_NOT_FOUND_MESSAGE }); + return; + } + + res.status(200).json({ + message: QN_RETRIEVED_MESSAGE, + question: formatQuestionResponse(randomQuestion[0]), + }); + } catch (error) { + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); + } +}; + export const readCategories = async ( _req: Request, res: Response, diff --git a/backend/question-service/src/routes/questionRoutes.ts b/backend/question-service/src/routes/questionRoutes.ts index a1a96c25cf..318f168620 100644 --- a/backend/question-service/src/routes/questionRoutes.ts +++ b/backend/question-service/src/routes/questionRoutes.ts @@ -7,6 +7,7 @@ import { readQuestionsList, readQuestionIndiv, readCategories, + readRandomQuestion, } from "../controllers/questionController.ts"; import { verifyAdminToken } from "../middlewares/basicAccessControl.ts"; @@ -22,6 +23,8 @@ router.get("/categories", readCategories); router.get("/", readQuestionsList); +router.get("/random", readRandomQuestion); + router.get("/:id", readQuestionIndiv); router.delete("/:id", verifyAdminToken, deleteQuestion); diff --git a/backend/question-service/src/utils/types.ts b/backend/question-service/src/utils/types.ts new file mode 100644 index 0000000000..228ac15643 --- /dev/null +++ b/backend/question-service/src/utils/types.ts @@ -0,0 +1,12 @@ +export type QnListSearchFilterParams = { + page: string; + qnLimit: string; + title?: string; + complexities?: string | string[]; + categories?: string | string[]; +}; + +export type RandomQnCriteria = { + category: string; + complexity: string; +}; diff --git a/backend/question-service/swagger.yml b/backend/question-service/swagger.yml index 249b6ea0b4..8cf992e10e 100644 --- a/backend/question-service/swagger.yml +++ b/backend/question-service/swagger.yml @@ -205,6 +205,50 @@ paths: application/json: schema: $ref: "#/definitions/ServerError" + /api/questions/random: + get: + tags: + - questions + summary: Get a random question based on specified criteria + description: Get a random question based on specified criteria + parameters: + - in: query + name: complexity + schema: + type: string + required: true + description: Question complexity filter + - in: query + name: category + schema: + type: string + required: true + description: Question category filter + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + question: + $ref: "#/definitions/Question" + 400: + description: Bad Request + content: + application/json: + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/definitions/ServerError" /api/questions/{id}: put: tags: From 2ab3709c40660b81eba48dfd6732239454c093b5 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Fri, 25 Oct 2024 22:04:57 +0800 Subject: [PATCH 006/192] Add collab service --- backend/collab-service/.env.sample | 7 +- backend/collab-service/README.md | 52 ++++ backend/collab-service/app.ts | 2 +- backend/collab-service/config/redis.ts | 18 ++ backend/collab-service/docs/image1.png | Bin 0 -> 21457 bytes backend/collab-service/docs/image2.png | Bin 0 -> 20104 bytes backend/collab-service/docs/image3.png | Bin 0 -> 28515 bytes backend/collab-service/docs/image4.png | Bin 0 -> 14934 bytes backend/collab-service/package-lock.json | 288 +++++++++++++++++- backend/collab-service/package.json | 2 + backend/collab-service/server.ts | 23 +- .../src/controllers/collabController.ts | 0 .../src/handlers/websocketHandler.ts | 72 +++++ frontend/src/contexts/MatchContext.tsx | 6 + frontend/src/pages/CollabSandbox/index.tsx | 7 +- 15 files changed, 469 insertions(+), 8 deletions(-) create mode 100644 backend/collab-service/README.md create mode 100644 backend/collab-service/config/redis.ts create mode 100644 backend/collab-service/docs/image1.png create mode 100644 backend/collab-service/docs/image2.png create mode 100644 backend/collab-service/docs/image3.png create mode 100644 backend/collab-service/docs/image4.png delete mode 100644 backend/collab-service/src/controllers/collabController.ts create mode 100644 backend/collab-service/src/handlers/websocketHandler.ts diff --git a/backend/collab-service/.env.sample b/backend/collab-service/.env.sample index 1800f50753..77915e9467 100644 --- a/backend/collab-service/.env.sample +++ b/backend/collab-service/.env.sample @@ -1,4 +1,9 @@ NODE_ENV=development SERVICE_PORT=3003 -ORIGINS=http://localhost:5173,http://127.0.0.1:5173 \ No newline at end of file +ORIGINS=http://localhost:5173,http://127.0.0.1:5173 + +REDIS_URI=redis://redis:6379 + +# Test +REDIS_URI_TEST=redis://test-redis:6379 diff --git a/backend/collab-service/README.md b/backend/collab-service/README.md new file mode 100644 index 0000000000..00ec0ce66b --- /dev/null +++ b/backend/collab-service/README.md @@ -0,0 +1,52 @@ +# Collab Service Guide + +## Setting-up Collab Service + +1. In the `collab-service` directory, create a copy of the `.env.sample` file and name it `.env`. + +2. Update the following variable in the `.env` file: + + - `REDIS_URI` + +## Running Collab Service Individually + +1. Set up and run Redis using `docker compose run --rm --name redis -p 6379:6379 redis`. + +2. Open Command Line/Terminal and navigate into the `collab-service` directory. + +3. Run the command: `npm install`. This will install all the necessary dependencies. + +4. Run the command `npm start` to start the Collab Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. + +## Running Collab Service Individually with Docker + +1. Open the command line/terminal. + +2. Run the command `docker compose run collab-service` to start up the collab service and its dependencies. + +## After running + +1. Using applications like Postman, you can interact with the Collab Service on port 3003. If you wish to change this, please update the `.env` file. + +2. Setting up Socket.IO connection on Postman: + - You should open 2 tabs on Postman to simulate 2 users in the Collab Service. + + - Select the `Socket.IO` option and set URL to `http://localhost:3003`. Click `Connect`. + ![image1.png](docs/image1.png) + + - Add the follow events in the `Events` tab and listen to them. + ![image2.png](docs/image2.png) + + - To send a message, go to the `Message` tab and ensure that your message is being parsed as `JSON`. + ![image3.png](docs/image3.png) + + - In the `Event name` input, input the correct event name. Click on `Send` to send a message. + ![image4.png](docs/image4.png) + +## Events Available +| Event Name | Description | Parameters | Response Event | +|----------------|-----------------------------------|-------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------| +| **join** | Joins a collaboration room. | `roomId` (string): ID of the room. | **room_full:** Emitted if the room is full (only 2 users allowed).
**connected:** Emitted upon successful connection. | +| **change** | Sends updated code to other user. | `roomId` (string): ID of the room.
`code` (string): Updated code content. | **code_change:** Emitted with the updated code content. | +| **leave** | Leaves the collaboration room. | `roomId` (string): ID of the room. | **left:** Emitted when one user leaves the room, notifying the other user. | +| **disconnect** | Disconnects from the server. | None | **disconnected:** Emitted when one user is disconnected, notifying the other user. | \ No newline at end of file diff --git a/backend/collab-service/app.ts b/backend/collab-service/app.ts index 3b35ab1247..0ccce35d8a 100644 --- a/backend/collab-service/app.ts +++ b/backend/collab-service/app.ts @@ -9,7 +9,7 @@ import collabRoutes from "./src/routes/collabRoutes.ts"; dotenv.config(); -const allowedOrigins = process.env.ORIGINS +export const allowedOrigins = process.env.ORIGINS ? process.env.ORIGINS.split(",") : ["http://localhost:5173", "http://127.0.0.1:5173"]; diff --git a/backend/collab-service/config/redis.ts b/backend/collab-service/config/redis.ts new file mode 100644 index 0000000000..6752ec0ab2 --- /dev/null +++ b/backend/collab-service/config/redis.ts @@ -0,0 +1,18 @@ +import { createClient } from "redis"; +import dotenv from "dotenv"; + +dotenv.config(); + +const REDIS_URI = + process.env.NODE_ENV === "test" + ? process.env.REDIS_URI_TEST + : process.env.REDIS_URI || "redis://localhost:6379"; + +const client = createClient({ url: REDIS_URI }); + +export const connectRedis = async () => { + await client.connect(); + client.on("error", (err) => console.log(`Error: ${err}`)); +}; + +export default client; diff --git a/backend/collab-service/docs/image1.png b/backend/collab-service/docs/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..d237c2f9b7c0ce7e95d8382bc8f9a04608954497 GIT binary patch literal 21457 zcmdSBc{r5)`!`OA7Svr)wicDUgxq#xX{BVTgzS|h%PiKhj}|FHn0;`KEXG>mPMF_#kWlLDSHf`OP068vS+7 zFZi#WkG`t9pFFht?%#iH+xD%=`nL2Fl7(Z9hYgeLIOr%e_(dqQ%MSm>lNo~TMZV3f zci@Cw-T-t2_`7yWyP%f;*XIbA(-B*KeLQ|r1h#hdun52MuZs!oOA#xyKRX9k$lD9hW}v z!S(uUWs65rxa_q7o1YOCAo*Z)FBeyh_ZNl3xe7R_A>3yU+S`V9N?lo+Uq0O&f8~$c zqhu-drIVjeUI*XDlqwy)OZ=KBTwPic8k*HwBL3@5rZOdUb9M{E;)t)8ml<%ZV&;Rx zGS(5&cf9VG$KD;l7CBilYh$s-QufHDg

}d{jlOEFF6hHuG7beB%4}PnFb?HsZ(- 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 literal 0 HcmV?d00001 diff --git a/backend/collab-service/docs/image2.png b/backend/collab-service/docs/image2.png new file mode 100644 index 0000000000000000000000000000000000000000..b8553cca43de3376a6deac200cc2fb734271f242 GIT binary patch literal 20104 zcmb@ucUTkax;Gp|L_t7Bbb%r^L_k1=NG}#ZdJUn2A~ithErcRXLBK+h-g`o5A+%5h zBvfgU7EpTVE!2=Vy7pRUzvrCq`}W!A`-AIYAT!r9Gtb=juT9=+sw*-va4-M>04Am9 z&$I!66IB2Jjp^xA)W0|a?FFarz3! zxx@My3t+3t`C@sUh{)5CmCuLXj9x@to~_J&o-+lE4?l+tzQm!abpDIxo13T3J@F3x z%J79-?%37C`jgsMR;BB;)AM8>rmh5WVONhb$*r++>mKpEuA9_JHQq4%PNAj#5+xVF zNF5Y!G*agpkaceA1ofXmsx_H8vI*2jQ2vUcuA%M%&A?nG$vRksd z8I-J_MB*yfaTm``NptAyfiB+;c(3VG^(=>HKx8e zepyeu5ZK_aTaItH?b?12ULK$6JgmNwao)d#HSm@!_^g;fue@_uveqsC~FEm2+~w43vA#`EA?=t=V0*OT6&wtBU( zTie1of(4HN(nQdGQT&-FLHZfig!E)EXwA4d%3oW9&_86V*92Sn_J^AP)LApouqB^m zZZe$AH&U34w}Nc6(blJzqg5VEVs)}sqrhJlbzQxbX9OL%@!rT4I)wt|ac=D@l^@5g zN#!ChMj7PKre!LLDt>3AnOZKG%vpIAczm2OrmnjX5)rK59qxXNQF&VI9tIb%JJ zPLPp!X;bIv%XVogj_=e|@|MiV{xIJs<08U1HN)qu@4n6ie;)pzqU6YoaZCV;S z_%xDuFn@wy3fVBx`7q{@L&DC`ZOy>B+#K80Jdgf2?73;OV(PNT*O~+Z=Cm3BcpTbd zUwftGQ=rF8abHv-N!t!S=2?p@$UcZzP}=IAM0gfu*PM+sfqv$o|J2??z0-+x?y=?7 z!PPTFP0LMuW}+d;6ZlQny)#3ym$F|>E;OlM#W=l;yhJJAyxi1(!Bri(xXufefFk1y)c6I&(~(y^^S_u{3|dQ4V70fW zpf3rN1j`}p2inQ`8%G1q`w+yYp1?e%3%}`Iyd$+glvt%jo=_kAW)Wcn4yZNdk1McZUeHY zL8EE^d`D8*oYp`uksAl|ejR7}Qb@Mih~E@$V34GYz?fAjBZU~+C7HifwXbPH*LZPY zE1aau#3-m}vKv!bnY1?utGzo*!8XE-JquJecB8eUV};S?)(~`AJtF3=vZrcd_6?J+ zN3oOhKX{N1hF|Okr!6k}2SVXnV%>Kbxan{Q4!L?)?YY5w&isuZG97)N$u>wU{HQ-m zvOW$lG&xs1eCImj5;8kYHuEvb`@Zau(B4`+dhnof)dj8H@!>R6ZRw}7$)fRR8KkAx zsBwAP3usv$BT01+&`(tAVBc3AiU(5YN{*7n^bkS-Jlv~?Tboe!+c?j?r>?uhhuuZE zZh_u53u)*PY0nU>&eB11ZC4eC#RVKz;BXVD^3bh#wuM;=Zdi{W)deexbx%!I(ngSF zA|K7SO6=B7!W4R!tBl{ac2?|}d+ozSoSSbm?f9pY{U_lx{z)_X$W(0JPr`-0Yc}}N zD@Pve2XC~Yky_?iX8`ZdI6=0Q=2|Q@DDL2qQ1R$k=_A>q&Zv>jNQ)v<0oY!tY`nwX z?$5G3u2cikTWsBOxoo^NseB%HiEX4><0_v~imd}BsnxQ!jxiiZI6$K(J$-pC@Miu- zq|Qk))^Dj{KYJPdHaeE^G#v zlh-dZB7wlMhNIH5z=5*PV~0E}=~cYi(NK^jD@C$&nZBW>(mowHv0+x;c?_~g?eZs^ zJlX_POh`h9Yh%71u>!}DYr}}J6_SX^)Qw1ki zKb0l0$QqN@T_bQuP}!{dF=ZiK3{ z(He1`bDuplZ7n^*XTH#9}XA$_@>xY`kL!?d+VS3eu~k*>FTsRK2B`#AFY zmjufqS{c9kr3^IUNW=(VU`8oh?xolf%Iyg(5ozVP!_LV#?0!>R8CkJNsCRy=P*yb1 zKh4A+UX!?#wI*%gXR`CKGgZ*2ZY+M&(VCET%xcxM$c30DJ67++1EXS*$6Q74K+%*= zoPSa#-7t(S8iDiJN6`*r=P|=e2et64fb}9XRyvjCDaggr<2A#LSgf2Q@~eR67c`!~ zG6(y$NY}e_bC49u#s2_ztUl44v&S`A@Lf91CIxe2UW-%O4z41LLVBXV?ZoNk$Vxn` z_equ*4r{d?9K0Os^Hd(Gu_vz;E>>*>t2-1LN(9omFxBU|u&;7t=5MB5SkmdSB88nN zexsH$+ihR}rcB^6p$XT~fj+(-3*2IfYtIFzHO3Ro(BUM}CB+E)=F`5$Mi1Z0m+5V)T0vr=@Xj*4X(11c( zw;x%PwOh>`@T`>!g+)JW3%~fjIUBv>En#anapeCBa(I|`mrVGcp1(6nUKLhWQWCes z9ewFBw>K$lHL`H;jV7bUd}g)Y)>u11W>H(@uY`k5EgxsQ!q6D-Mi_rCEV#lo8r7X| z9l0rEi@dRWGT9Ye_tCLZ-KqM(?D9)TC2e21A$z5_?)7TrnVSRlCV9Ep{MG)#X_iam z``a6C(k>P-hYml3PdrkEiactdxiU4ELs^SdDFJc`GW4iwvwA;z%mNLYkl-x23{)PB ztZDIY?h?lyXfLhR$jjexAUo)xJW~!Z)eqL878C4U;*OK1v!0=jPh%4u$=zh&Ho&~d zGoUE{28Y`>X8wG%(a%-=LbkCi5gVsj#`g%7jIkPy8p?hln{f_ERihzCZ8Re@<&Fk) z!geCxgTmci(C~AwH+ew%2QyvM`t&5K)3UD=ipaI#9`Y zbFd>7X(paz0AxwP#^`UnF_?Kj2HZwLA*H*kE0rhkSD857lZ3wBBS0p^7=ZI1o^ccO z9z=S1WK_5Xt>ylVOKcLSw@^@qIA$I=>u zWtke5)J<3?v>Xtg7DUB5skxpfma)OXm;%3>U%k6f9*$=|` zp&43yyTeifUhvS|#NDB6MIbB%TWAb(=YyL5#P_Y({ou%+`5`!qwWoVzs{@?wzB;>h zHnlP$kY$-pz2i9VEYng8u~H@hD(#T+DWG>rb!&a8l5>_tI(D@S+>?}myT9LmZAf^H zF4)Sf8mR$ikxvo|pfNtr1`Zjh7OZGUcWI%;?|zJ3zzuX3MDwkLu;YA$%xWQaJie6% zG?r^u+HCp1395ra<~o2^?uh1a~wC^m!(0CB;ILofO~(}y6A+p3H~Z5 zku!_$n66*)!5%*vPWeH4)wK%6hk3g}E6C2K1wR`u={~;_I3TTj4XFHdnNfdlHY>(au%hG4u%$ypnOUV^zJG;K zr8|;h7h+m@ViIwJcMj%hv)s$WOWt@^OTs)^+^9kMQW3KOX!pH=IV+wy$Q|~w_>zA~ zFN2Ni4#6yp7`Jo+T60F(XHWvldLp+&SY&tS_D-3rh_1eGVVSIH&VKJi7XMCZE>G*? zTmb|$Pd;iCTvE^rd-NcD(M$EpZnB}t6%UTJ@4^L3NDaG#?M0G^nvjg+gct29A&HQT zsECgz%DqZs{_I&y5J>^)|iOasRQ5ybMge8%4Yv`nxJ1E{!?_9lyns?(e z+8rXN_Kb{UA2;Uk-F-fbE!Fo{nbcF?l;$9!MwZSY7b+#Q9nUCN37Xf~1)TE5fTy{9 z9!T>C%NK%=oFaWUxx4*uslD*`GO0%E&ZJHrKUrMB$G)DTr6d=|$Q~nI`d}^%%vT+= zWK1MR8Sm+t*O^YRoiDlTwK6=c=GaOQ*0OL^()3NcYu1nvs3urA)VD&^Gt{=Dd!$8# zuV`{hZ)f+fEh0HP<5D+I!u!HYWKdx$v*Ap82*ZKd($bovvSPwgUz}RfMVapm_G|2! zd&dvLokEI+Is)D-o`7%(RN8b}Nji*`0!#eL&g1E}zBJ^ao3niQf5hF6R8axDpg0c> zH|L}~jV?=PRZPyuidq&fSFMp}rA76k-UQ(7bB~AjInTMyaea(f&MX#Sh@mLWK6hW5 z<8yD%;pP+nW_bQVDTmA9A34W!=b7wcQ8`>gE(z-HlM_lqAt?5N_E?rufl|{u&@O{&c zc6lV~k2i_U#nCa=+I{{g)7w`yzbWEk^BOZxiz+$EExoMO_6cR

Ss`Nf+mJuGz=z znW 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 KgE>KQIe!C94Wv;3 literal 0 HcmV?d00001 diff --git a/backend/collab-service/docs/image3.png b/backend/collab-service/docs/image3.png new file mode 100644 index 0000000000000000000000000000000000000000..1a80ceeec92fafb9bf8b01dbb62f7fae4946e317 GIT binary patch literal 28515 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 8" } }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1801,6 +1861,11 @@ "@sinonjs/commons": "^3.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==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1867,11 +1932,15 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1983,7 +2052,6 @@ "version": "22.7.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -2643,6 +2711,14 @@ "dev": true, "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==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -2875,6 +2951,14 @@ "node": ">=12" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3204,6 +3288,63 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.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==", + "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==", + "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==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3891,6 +4032,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5830,6 +5979,19 @@ "dev": true, "license": "MIT" }, + "node_modules/redis": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.0", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6114,6 +6276,107 @@ "node": ">=8" } }, + "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==", + "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==", + "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==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "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==", + "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==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "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==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6461,7 +6724,6 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -6622,6 +6884,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.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==", + "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/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index 552a3b9808..88c4f39f8e 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -17,6 +17,8 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", + "redis": "^4.7.0", + "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", "yaml": "^2.6.0" }, diff --git a/backend/collab-service/server.ts b/backend/collab-service/server.ts index 34443b0614..c8ac91418f 100644 --- a/backend/collab-service/server.ts +++ b/backend/collab-service/server.ts @@ -1,9 +1,28 @@ -import app from "./app"; +import http from "http"; +import app, { allowedOrigins } from "./app.ts"; +import { handleWebsocketCollabEvents } from "./src/handlers/websocketHandler"; +import { Server } from "socket.io"; +import { connectRedis } from "./config/redis.ts"; + +const server = http.createServer(app); +export const io = new Server(server, { + cors: { + origin: allowedOrigins, + methods: ["GET", "POST"], + }, + connectionStateRecovery: {}, +}); + +io.on("connection", (socket) => { + handleWebsocketCollabEvents(socket); +}); const PORT = process.env.SERVICE_PORT || 3003; if (process.env.NODE_ENV !== "test") { - app.listen(PORT, () => { + server.listen(PORT, () => { console.log(`Collab service server listening on http://localhost:${PORT}`); }); + + connectRedis(); } diff --git a/backend/collab-service/src/controllers/collabController.ts b/backend/collab-service/src/controllers/collabController.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts new file mode 100644 index 0000000000..6bf41d8329 --- /dev/null +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -0,0 +1,72 @@ +import { Socket } from "socket.io"; +import { io } from "../../server"; +import redisClient from "../../config/redis"; + +enum CollabEvents { + // Receive + JOIN = "join", + CHANGE = "change", + LEAVE = "leave", + DISCONNECT = "disconnect", + + // Send + ROOM_FULL = "room_full", + CONNECTED = "connected", + CODE_CHANGE = "code_change", + LEFT = "left", + DISCONNECTED = "disconnected", +} + +const EXPIRY_TIME = 3600; + +export const handleWebsocketCollabEvents = (socket: Socket) => { + socket.on(CollabEvents.JOIN, async ({ roomId }) => { + if (!roomId) { + return; + } + + const room = io.sockets.adapter.rooms.get(roomId); + if (room && room.size >= 2) { + socket.emit(CollabEvents.ROOM_FULL); + return; + } + + socket.join(roomId); + socket.data.roomId = roomId; + + // in case of disconnect, send the code to the user when rejoin + const code = await redisClient.get(`collaboration:${roomId}`); + if (code) { + io.to(roomId).emit(CollabEvents.CONNECTED, { code }); + } else { + io.to(roomId).emit(CollabEvents.CONNECTED); + } + }); + + socket.on(CollabEvents.CHANGE, async ({ roomId, code }) => { + if (!roomId || !code) { + return; + } + + await redisClient.set(`collaboration:${roomId}`, code, { + EX: EXPIRY_TIME, + }); + io.to(roomId).emit(CollabEvents.CODE_CHANGE, { code }); + }); + + socket.on(CollabEvents.LEAVE, ({ roomId }) => { + if (!roomId) { + return; + } + + socket.leave(roomId); + socket.to(roomId).emit(CollabEvents.LEFT); + }); + + socket.on(CollabEvents.DISCONNECT, () => { + const { roomId } = socket.data; + if (roomId) { + socket.to(roomId).emit(CollabEvents.DISCONNECTED); + } + }); +}; diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 8d6743440c..9c28b9e2f0 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -81,6 +81,7 @@ type MatchContextType = { matchingTimeout: () => void; matchOfferTimeout: () => void; verifyMatchStatus: () => void; + getMatchId: () => string | null; matchUser: MatchUser | null; matchCriteria: MatchCriteria | null; partner: MatchUser | null; @@ -476,6 +477,10 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { ); }; + const getMatchId = () => { + return matchId; + }; + return ( = (props) => { matchingTimeout, matchOfferTimeout, verifyMatchStatus, + getMatchId, matchUser, matchCriteria, partner, diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 3bc93e2170..b806028cdf 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -12,10 +12,15 @@ const CollabSandbox: React.FC = () => { if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { stopMatch, verifyMatchStatus, partner, loading } = match; + const { stopMatch, verifyMatchStatus, getMatchId, partner, loading } = match; useEffect(() => { verifyMatchStatus(); + + // TODO + // connect to collab service using getMatchId() + console.log(getMatchId()); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From be7c768acfb629fd5c95c5f5a2f68fb7a519e9d0 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Fri, 25 Oct 2024 22:06:58 +0800 Subject: [PATCH 007/192] Update package.json --- backend/collab-service/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index 88c4f39f8e..e2b7d4ad94 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -2,6 +2,7 @@ "name": "collab-service", "version": "1.0.0", "main": "server.ts", + "type": "module", "scripts": { "start": "tsx server.ts", "dev": "tsx watch server.ts", From 8130096b5196d1864bf223b88cf2c92fe6f12a37 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:07:30 +0800 Subject: [PATCH 008/192] Add code execution service --- backend/code-execution-service/.dockerignore | 5 + backend/code-execution-service/.env.sample | 8 + backend/code-execution-service/Dockerfile | 13 + backend/code-execution-service/app.ts | 35 + .../code-execution-service/eslint.config.js | 10 + .../code-execution-service/package-lock.json | 6695 +++++++++++++++++ backend/code-execution-service/package.json | 38 + backend/code-execution-service/server.ts | 11 + .../controllers/codeExecutionControllers.ts | 20 + .../src/routes/codeExecutionRoutes.ts | 8 + .../src/utils/oneCompilerApi.ts | 29 + backend/code-execution-service/swagger.yml | 23 + backend/code-execution-service/tsconfig.json | 110 + docker-compose.yml | 15 + 14 files changed, 7020 insertions(+) create mode 100644 backend/code-execution-service/.dockerignore create mode 100644 backend/code-execution-service/.env.sample create mode 100644 backend/code-execution-service/Dockerfile create mode 100644 backend/code-execution-service/app.ts create mode 100644 backend/code-execution-service/eslint.config.js create mode 100644 backend/code-execution-service/package-lock.json create mode 100644 backend/code-execution-service/package.json create mode 100644 backend/code-execution-service/server.ts create mode 100644 backend/code-execution-service/src/controllers/codeExecutionControllers.ts create mode 100644 backend/code-execution-service/src/routes/codeExecutionRoutes.ts create mode 100644 backend/code-execution-service/src/utils/oneCompilerApi.ts create mode 100644 backend/code-execution-service/swagger.yml create mode 100644 backend/code-execution-service/tsconfig.json diff --git a/backend/code-execution-service/.dockerignore b/backend/code-execution-service/.dockerignore new file mode 100644 index 0000000000..4abc77f632 --- /dev/null +++ b/backend/code-execution-service/.dockerignore @@ -0,0 +1,5 @@ +coverage +node_modules +tests +.env* +*.md diff --git a/backend/code-execution-service/.env.sample b/backend/code-execution-service/.env.sample new file mode 100644 index 0000000000..77b50686f8 --- /dev/null +++ b/backend/code-execution-service/.env.sample @@ -0,0 +1,8 @@ +NODE_ENV=development +SERVICE_PORT=3004 + +ORIGINS=http://localhost:5173,http://127.0.0.1:5173 + +# One Compiler +ONE_COMPILER_URL= +ONE_COMPILER_KEY= \ No newline at end of file diff --git a/backend/code-execution-service/Dockerfile b/backend/code-execution-service/Dockerfile new file mode 100644 index 0000000000..00840fdb06 --- /dev/null +++ b/backend/code-execution-service/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /code-execution-service + +COPY package*.json . + +RUN npm ci + +COPY . . + +EXPOSE 3003 + +CMD ["npm", "run", "dev"] diff --git a/backend/code-execution-service/app.ts b/backend/code-execution-service/app.ts new file mode 100644 index 0000000000..59942f969e --- /dev/null +++ b/backend/code-execution-service/app.ts @@ -0,0 +1,35 @@ +import express, { Request, Response } from "express"; +import dotenv from "dotenv"; +import fs from "fs"; +import yaml from "yaml"; +import swaggerUi from "swagger-ui-express"; +import cors from "cors"; + +import codeExecutionRoutes from "./src/routes/codeExecutionRoutes.ts"; + +dotenv.config(); + +const allowedOrigins = process.env.ORIGINS + ? process.env.ORIGINS.split(",") + : ["http://localhost:5173", "http://127.0.0.1:5173"]; + +const file = fs.readFileSync("./swagger.yml", "utf-8"); +const swaggerDocument = yaml.parse(file); + +const app = express(); + +app.use(cors({ origin: allowedOrigins, credentials: true })); + +app.options("*", cors({ origin: allowedOrigins, credentials: true })); + +app.use(express.json()); + +app.use("/api/run", codeExecutionRoutes); + +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + +app.get("/", (req: Request, res: Response) => { + res.status(200).json({ message: "Hello world from code execution service" }); +}); + +export default app; diff --git a/backend/code-execution-service/eslint.config.js b/backend/code-execution-service/eslint.config.js new file mode 100644 index 0000000000..3c8af371cd --- /dev/null +++ b/backend/code-execution-service/eslint.config.js @@ -0,0 +1,10 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + { files: ["**/*.{js,mjs,cjs,ts}"] }, + { languageOptions: { globals: globals.node } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; diff --git a/backend/code-execution-service/package-lock.json b/backend/code-execution-service/package-lock.json new file mode 100644 index 0000000000..11924dc89a --- /dev/null +++ b/backend/code-execution-service/package-lock.json @@ -0,0 +1,6695 @@ +{ + "name": "code-execution-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-execution-service", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.7.7", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.6.0" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.7.9", + "@types/swagger-ui-express": "^4.1.6", + "cross-env": "^7.0.3", + "eslint": "^9.13.0", + "globals": "^15.11.0", + "jest": "^29.7.0", + "tsx": "^4.19.1", + "typescript": "^5.6.3", + "typescript-eslint": "^8.11.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.9.tgz", + "integrity": "sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.25.9", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.9.tgz", + "integrity": "sha512-yD+hEuJ/+wAJ4Ox2/rpNv5HIuPG82x3ZlQvYVn8iYCprdxzE7P1udpGF1jyjQVBU4dgznN+k2h103vxZ7NdPyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.9.tgz", + "integrity": "sha512-WYvQviPw+Qyib0v92AwNIrdLISTp7RfDkM7bPqBvpbnhY4wq8HvHBZREVdYDXk98C8BkOIVnHAY3yvj7AVISxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helpers": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "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/@babel/generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.9.tgz", + "integrity": "sha512-omlUGkr5EaoIJrhLf9CJ0TvjBRpd9+AXRG//0GEQ9THSo8wPiTlbpy1/Ow8ZTrbXpjd9FHXfbFQx32I04ht0FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.9.tgz", + "integrity": "sha512-TvLZY/F3+GvdRYFZFyxMvnsKi+4oJdgZzU3BoGN9Uc2d9C6zfNwJcKKhjqLAhK8i46mv93jsO74fDh3ih6rpHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-simple-access": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", + "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.9.tgz", + "integrity": "sha512-oKWp3+usOJSzDZOucZUAMayhPz/xVjzymyDzUN8dk0Wd3RWMlGLXi07UCQ/CgQVb8LvXx3XBajJH4XGgkt7H7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", + "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.9.tgz", + "integrity": "sha512-u3EN9ub8LyYvgTnrgp8gboElouayiwPdnM7x5tcnW3iSt09/lQYPwMNK40I9IUxo7QOZhAsPHCmmuO7EPdruqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "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/@babel/types": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", + "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "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/@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "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" + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "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/@eslint/js": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz", + "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "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", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", + "integrity": "sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/type-utils": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz", + "integrity": "sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "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/@typescript-eslint/scope-manager": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", + "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.11.0.tgz", + "integrity": "sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "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/@typescript-eslint/types": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", + "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", + "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "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/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", + "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz", + "integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "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" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.45", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.45.tgz", + "integrity": "sha512-vOzZS6uZwhhbkZbcRyiy99Wg+pYFV5hk+5YaECvx0+Z31NR3Tt5zS6dze2OepT6PCTzVzT0dIJItti+uAW5zmw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", + "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.13.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/ms": { + "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/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "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/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-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" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "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/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tsx": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz", + "integrity": "sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.11.0.tgz", + "integrity": "sha512-cBRGnW3FSlxaYwU8KfAewxFK5uzeOAp0l2KebIlPDOT5olVi65KDG/yjBooPBG0kGW/HLkoz1c/iuBFehcS3IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.11.0", + "@typescript-eslint/parser": "8.11.0", + "@typescript-eslint/utils": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.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/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/code-execution-service/package.json b/backend/code-execution-service/package.json new file mode 100644 index 0000000000..d4dbd84400 --- /dev/null +++ b/backend/code-execution-service/package.json @@ -0,0 +1,38 @@ +{ + "name": "code-execution-service", + "version": "1.0.0", + "main": "server.ts", + "scripts": { + "start": "tsx server.ts", + "dev": "tsx watch server.ts", + "test": "cross-env NODE_ENV=test && jest", + "lint": "eslint ." + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "axios": "^1.7.7", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.6.0" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.7.9", + "@types/swagger-ui-express": "^4.1.6", + "cross-env": "^7.0.3", + "eslint": "^9.13.0", + "globals": "^15.11.0", + "jest": "^29.7.0", + "tsx": "^4.19.1", + "typescript": "^5.6.3", + "typescript-eslint": "^8.11.0" + } +} diff --git a/backend/code-execution-service/server.ts b/backend/code-execution-service/server.ts new file mode 100644 index 0000000000..27a381b398 --- /dev/null +++ b/backend/code-execution-service/server.ts @@ -0,0 +1,11 @@ +import app from "./app"; + +const PORT = process.env.SERVICE_PORT || 3003; + +if (process.env.NODE_ENV !== "test") { + app.listen(PORT, () => { + console.log( + `Code Execution service server listening on http://localhost:${PORT}` + ); + }); +} diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts new file mode 100644 index 0000000000..482a5d0b4f --- /dev/null +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -0,0 +1,20 @@ +import { Request, Response } from "express"; +import { oneCompilerApi } from "../utils/oneCompilerApi"; + +export const executeCode = async (req: Request, res: Response) => { + const { language, stdin, files } = req.body; + + if (!language || !stdin || !files) { + res + .status(400) + .json({ error: "Missing required fields: language, stdin, or files" }); + } + + try { + const response = await oneCompilerApi(language, stdin, files); + res.json(response.data); + } catch (error) { + console.error("Error executing code:", error); + res.status(500).json({ error: "Failed to execute code" }); + } +}; diff --git a/backend/code-execution-service/src/routes/codeExecutionRoutes.ts b/backend/code-execution-service/src/routes/codeExecutionRoutes.ts new file mode 100644 index 0000000000..59a3f3281d --- /dev/null +++ b/backend/code-execution-service/src/routes/codeExecutionRoutes.ts @@ -0,0 +1,8 @@ +import express from "express"; +import { executeCode } from "../controllers/codeExecutionControllers"; + +const router = express.Router(); + +router.post("/", executeCode); + +export default router; diff --git a/backend/code-execution-service/src/utils/oneCompilerApi.ts b/backend/code-execution-service/src/utils/oneCompilerApi.ts new file mode 100644 index 0000000000..fa9dd48fb9 --- /dev/null +++ b/backend/code-execution-service/src/utils/oneCompilerApi.ts @@ -0,0 +1,29 @@ +import axios from "axios"; +import dotenv from "dotenv"; + +dotenv.config(); + +export const oneCompilerApi = async ( + language: String, + stdin: String, + files: String +) => { + const response = await axios.post( + process.env.ONE_COMPILER_URL || + "https://onecompiler-apis.p.rapidapi.com/api/v1/run", + { + language, + stdin, + files, + }, + { + headers: { + "Content-Type": "application/json", + "x-rapidapi-host": "onecompiler-apis.p.rapidapi.com", + "x-rapidapi-key": process.env.ONE_COMPILER_KEY, + }, + } + ); + + return response; +}; diff --git a/backend/code-execution-service/swagger.yml b/backend/code-execution-service/swagger.yml new file mode 100644 index 0000000000..805be03337 --- /dev/null +++ b/backend/code-execution-service/swagger.yml @@ -0,0 +1,23 @@ +openapi: 3.0.0 + +info: + title: Code Execution Service + version: 1.0.0 + +paths: + /: + get: + tags: + - root + summary: Root + description: Ping the server + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string diff --git a/backend/code-execution-service/tsconfig.json b/backend/code-execution-service/tsconfig.json new file mode 100644 index 0000000000..830b218c6e --- /dev/null +++ b/backend/code-execution-service/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "noEmit": true /* Disable emitting files from a compilation. */, + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 629a715847..340ee2df26 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,6 +70,21 @@ services: - /collab-service/node_modules restart: on-failure + code-execution-service: + image: peerprep/code-execution-service + build: ./backend/code-execution-service + environment: + - CHOKIDAR_USEPOLLING=true + env_file: ./backend/code-execution-service/.env + ports: + - 3004:3004 + networks: + - peerprep-network + volumes: + - ./backend/code-execution-service:/code-execution-service + - /code-execution-service/node_modules + restart: on-failure + frontend: image: peerprep/frontend build: ./frontend From 47a348f4836361aa04b87d5b7cd1110b2d2f9ce8 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:09:05 +0800 Subject: [PATCH 009/192] Update readme --- backend/collab-service/README.md | 2 +- frontend/src/pages/CollabSandbox/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/collab-service/README.md b/backend/collab-service/README.md index 00ec0ce66b..3c3b8a5b5b 100644 --- a/backend/collab-service/README.md +++ b/backend/collab-service/README.md @@ -34,7 +34,7 @@ - Select the `Socket.IO` option and set URL to `http://localhost:3003`. Click `Connect`. ![image1.png](docs/image1.png) - - Add the follow events in the `Events` tab and listen to them. + - Add the following events in the `Events` tab and listen to them. ![image2.png](docs/image2.png) - To send a message, go to the `Message` tab and ensure that your message is being parsed as `JSON`. diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index b806028cdf..15cfc20039 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -18,7 +18,7 @@ const CollabSandbox: React.FC = () => { verifyMatchStatus(); // TODO - // connect to collab service using getMatchId() + // use getMatchId() as the room id in the collab service console.log(getMatchId()); // eslint-disable-next-line react-hooks/exhaustive-deps From d74ef75febb15e682c912ed0d1050f0c0bff0962 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sat, 26 Oct 2024 21:26:13 +0800 Subject: [PATCH 010/192] Set up real-time code editor --- backend/collab-service/app.ts | 2 +- backend/collab-service/package-lock.json | 215 +++- backend/collab-service/package.json | 3 + backend/collab-service/server.ts | 67 +- frontend/package-lock.json | 1034 +++++++++++++++++- frontend/package.json | 11 +- frontend/src/components/CodeEditor/index.tsx | 93 ++ frontend/src/pages/CollabSandbox/index.tsx | 6 +- frontend/src/utils/collabCursor.ts | 214 ++++ frontend/src/utils/collabSocket.ts | 143 +++ 10 files changed, 1775 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/CodeEditor/index.tsx create mode 100644 frontend/src/utils/collabCursor.ts create mode 100644 frontend/src/utils/collabSocket.ts diff --git a/backend/collab-service/app.ts b/backend/collab-service/app.ts index 3b35ab1247..0ccce35d8a 100644 --- a/backend/collab-service/app.ts +++ b/backend/collab-service/app.ts @@ -9,7 +9,7 @@ import collabRoutes from "./src/routes/collabRoutes.ts"; dotenv.config(); -const allowedOrigins = process.env.ORIGINS +export const allowedOrigins = process.env.ORIGINS ? process.env.ORIGINS.split(",") : ["http://localhost:5173", "http://127.0.0.1:5173"]; diff --git a/backend/collab-service/package-lock.json b/backend/collab-service/package-lock.json index 6a4b1388a1..3e7f67f819 100644 --- a/backend/collab-service/package-lock.json +++ b/backend/collab-service/package-lock.json @@ -9,11 +9,14 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@codemirror/collab": "^6.1.1", + "@codemirror/state": "^6.4.1", "axios": "^1.7.7", "body-parser": "^1.20.3", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", + "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", "yaml": "^2.6.0" }, @@ -700,6 +703,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@codemirror/collab": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/collab/-/collab-6.1.1.tgz", + "integrity": "sha512-tkIn9Jguh98ie12dbBuba3lE8LHUkaMrIFuCVeVGhncSczFdKmX25vC12+58+yqQW5AXi3py6jWY0W+jelyglA==", + "dependencies": { + "@codemirror/state": "^6.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -1801,6 +1817,11 @@ "@sinonjs/commons": "^3.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==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1867,11 +1888,15 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1983,7 +2008,6 @@ "version": "22.7.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -2643,6 +2667,14 @@ "dev": true, "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==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -3204,6 +3236,63 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.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==", + "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==", + "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==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6114,6 +6203,107 @@ "node": ">=8" } }, + "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==", + "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==", + "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==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "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==", + "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==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "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==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6461,7 +6651,6 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -6622,6 +6811,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.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==", + "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/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index 552a3b9808..8424e222c6 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -12,11 +12,14 @@ "license": "ISC", "description": "", "dependencies": { + "@codemirror/collab": "^6.1.1", + "@codemirror/state": "^6.4.1", "axios": "^1.7.7", "body-parser": "^1.20.3", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", + "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", "yaml": "^2.6.0" }, diff --git a/backend/collab-service/server.ts b/backend/collab-service/server.ts index 34443b0614..a11248cb49 100644 --- a/backend/collab-service/server.ts +++ b/backend/collab-service/server.ts @@ -1,9 +1,72 @@ -import app from "./app"; +import http from "http"; +import app, { allowedOrigins } from "./app.ts"; +import { Server, Socket } from "socket.io"; +import { ChangeSet, Text } from "@codemirror/state"; +import { Update } from "@codemirror/collab"; + +let updates: Update[] = []; +let doc = Text.of(["Start document"]); +let pending: ((value: any) => void)[] = []; + +const server = http.createServer(app); +export const io = new Server(server, { + cors: { + origin: allowedOrigins, + methods: ["GET", "POST"], + }, + connectionStateRecovery: {}, +}); + +io.on("connection", (socket: Socket) => { + socket.on("pullUpdates", (version: number) => { + if (version < updates.length) { + socket.emit("pullUpdateResponse", JSON.stringify(updates.slice(version))); + } else { + pending.push((updates) => { + socket.emit( + "pullUpdateResponse", + JSON.stringify(updates.slice(version)) + ); + }); + } + }); + + socket.on("pushUpdates", (version, docUpdates) => { + docUpdates = JSON.parse(docUpdates); + + try { + if (version != updates.length) { + socket.emit("pushUpdateResponse", false); + } else { + for (let update of docUpdates) { + let changes = ChangeSet.fromJSON(update.changes); + updates.push({ + changes, + clientID: update.clientID, + effects: update.effects, // cursor + }); + doc = changes.apply(doc); + } + socket.emit("pushUpdateResponse", true); + + while (pending.length) { + pending.pop()!(updates); + } + } + } catch (error) { + console.error(error); + } + }); + + socket.on("getDocument", () => { + socket.emit("getDocumentResponse", updates.length, doc.toString()); + }); +}); const PORT = process.env.SERVICE_PORT || 3003; if (process.env.NODE_ENV !== "test") { - app.listen(PORT, () => { + server.listen(PORT, () => { console.log(`Collab service server listening on http://localhost:${PORT}`); }); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5833bd7fd3..d6ab181bd5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,13 +11,19 @@ "@babel/preset-env": "^7.25.4", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", + "@codemirror/collab": "^6.1.1", + "@codemirror/lang-javascript": "^6.2.2", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", "@mui/icons-material": "^6.1.0", "@mui/material": "^6.1.0", + "@uiw/codemirror-extensions-basic-setup": "^4.23.6", + "@uiw/codemirror-extensions-langs": "^4.23.6", + "@uiw/react-codemirror": "^4.23.6", "@uiw/react-md-editor": "^4.0.4", "axios": "^1.7.7", + "codemirror": "^6.0.1", "history": "^5.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -26,7 +32,10 @@ "react-router-dom": "^6.3.0", "react-toastify": "^10.0.5", "socket.io-client": "^4.8.0", - "vite-plugin-svgr": "^4.2.0" + "vite-plugin-svgr": "^4.2.0", + "y-codemirror.next": "^0.3.5", + "y-webrtc": "^10.3.0", + "yjs": "^13.6.20" }, "devDependencies": { "@eslint/js": "^9.9.0", @@ -1934,6 +1943,392 @@ "dev": true, "license": "MIT" }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz", + "integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/collab": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/collab/-/collab-6.1.1.tgz", + "integrity": "sha512-tkIn9Jguh98ie12dbBuba3lE8LHUkaMrIFuCVeVGhncSczFdKmX25vC12+58+yqQW5AXi3py6jWY0W+jelyglA==", + "dependencies": { + "@codemirror/state": "^6.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz", + "integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-angular": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.3.tgz", + "integrity": "sha512-xgeWGJQQl1LyStvndWtruUvb4SnBZDAu/gvFH/ZU+c0W25tQR8e5hq7WTwiIY2dNxnf+49mRiGI/9yxIwB6f5w==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.3" + } + }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz", + "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz", + "integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-go": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz", + "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/go": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", + "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", + "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", + "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-less": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz", + "integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-lezer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-lezer/-/lang-lezer-6.0.1.tgz", + "integrity": "sha512-WHwjI7OqKFBEfkunohweqA5B/jIlxaZso6Nl3weVckz8EafYbPZldQEKSDb4QQ9H9BUkle4PVELP4sftKoA0uQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/lezer": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-liquid": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.2.1.tgz", + "integrity": "sha512-J1Mratcm6JLNEiX+U2OlCDTysGuwbHD76XwuL5o5bo9soJtSbz2g6RU3vGHFyS5DC8rgVmFSzi7i6oBftm7tnA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.0.tgz", + "integrity": "sha512-lYrI8SdL/vhd0w0aHIEvIRLRecLF7MiiRfzXFZY94dFwHqC9HtgxgagJ8fyYNBldijGatf9wkms60d8SrAj6Nw==", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz", + "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.6.tgz", + "integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz", + "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", + "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/sass": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz", + "integrity": "sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-vue": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", + "integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-wast": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz", + "integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.1.tgz", + "integrity": "sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", + "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/language-data": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.1.tgz", + "integrity": "sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==", + "dependencies": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-go": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.0", + "@codemirror/lang-liquid": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/lang-yaml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.4.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.4.1.tgz", + "integrity": "sha512-vdg3XY7OAs5uLDx2Iw+cGfnwtd7kM+Et/eMsqAGTfT/JKiVBQZXosTzjEbWAi/FrY6DcQIz8mQjBozFHZEUWQA==", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", + "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.34.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz", + "integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3455,6 +3850,175 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + }, + "node_modules/@lezer/cpp": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.2.tgz", + "integrity": "sha512-macwKtyeUO0EW86r3xWQCzOV9/CF8imJLpJlPv3sDY57cPGeUZ8gXWOWNlJr52TVByMV3PayFQCA5SHEERDmVQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/css": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz", + "integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/go": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.0.tgz", + "integrity": "sha512-co9JfT3QqX1YkrMmourYw2Z8meGC50Ko4d54QEcQbEYpvdUvN4yb0NBZdn/9ertgvjsySxHsKzH3lbm3vqJ4Jw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", + "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/java": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", + "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.4.19", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz", + "integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", + "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lezer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lezer/lezer/-/lezer-1.1.2.tgz", + "integrity": "sha512-O8yw3CxPhzYHB1hvwbdozjnAslhhR8A5BH7vfEMof0xk3p+/DFDfZkA9Tde6J+88WgtwaHy4Sy6ThZSkaI0Evw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.3.1.tgz", + "integrity": "sha512-DGlzU/i8DC8k0uz1F+jeePrkATl0jWakauTzftMQOcbaMkHbNSRki/4E2tOzJWsVpoKYhe7iTJ03aepdwVUXUA==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.2.tgz", + "integrity": "sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.14.tgz", + "integrity": "sha512-ykDOb2Ti24n76PJsSa4ZoDF0zH12BSw1LGfQXCYJhJyOGiFTfGaX0Du66Ze72R+u/P35U+O6I9m8TFXov1JzsA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", + "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/sass": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.0.7.tgz", + "integrity": "sha512-8HLlOkuX/SMHOggI2DAsXUw38TuURe+3eQ5hiuk9QmYOUyC55B1dYEIMkav5A4IELVaW4e1T4P9WRiI5ka4mdw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.5.tgz", + "integrity": "sha512-VFouqOzmUWfIg+tfmpcdV33ewtK+NSwd4ngSe1aG7HFb4BN0ExyY1b8msp+ndFrnlG4V4iC8yXacjFtrwERnaw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.1.tgz", @@ -3676,6 +4240,23 @@ } } }, + "node_modules/@nextjournal/lang-clojure": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nextjournal/lang-clojure/-/lang-clojure-1.0.0.tgz", + "integrity": "sha512-gOCV71XrYD0DhwGoPMWZmZ0r92/lIHsqQu9QWdpZYYBwiChNwMO4sbVMP7eTuAqffFB2BTtCSC+1skSH9d3bNg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@nextjournal/lezer-clojure": "1.0.0" + } + }, + "node_modules/@nextjournal/lezer-clojure": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nextjournal/lezer-clojure/-/lezer-clojure-1.0.0.tgz", + "integrity": "sha512-VZyuGu4zw5mkTOwQBTaGVNWmsOZAPw5ZRxu1/Knk/Xfs7EDBIogwIs5UXTYkuECX5ZQB8eOB+wKA2pc7VyqaZQ==", + "dependencies": { + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3720,6 +4301,63 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@replit/codemirror-lang-csharp": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-csharp/-/codemirror-lang-csharp-6.2.0.tgz", + "integrity": "sha512-6utbaWkoymhoAXj051mkRp+VIJlpwUgCX9Toevz3YatiZsz512fw3OVCedXQx+WcR0wb6zVHjChnuxqfCLtFVQ==", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@replit/codemirror-lang-nix": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-nix/-/codemirror-lang-nix-6.0.1.tgz", + "integrity": "sha512-lvzjoYn9nfJzBD5qdm3Ut6G3+Or2wEacYIDJ49h9+19WSChVnxv4ojf+rNmQ78ncuxIt/bfbMvDLMeMP0xze6g==", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@replit/codemirror-lang-solidity": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-solidity/-/codemirror-lang-solidity-6.0.2.tgz", + "integrity": "sha512-/dpTVH338KFV6SaDYYSadkB4bI/0B0QRF/bkt1XS3t3QtyR49mn6+2k0OUQhvt2ZSO7kt10J+OPilRAtgbmX0w==", + "dependencies": { + "@lezer/highlight": "^1.2.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@replit/codemirror-lang-svelte": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-svelte/-/codemirror-lang-svelte-6.0.0.tgz", + "integrity": "sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.1", + "@codemirror/lang-html": "^6.2.0", + "@codemirror/lang-javascript": "^6.1.1", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/javascript": "^1.2.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", @@ -4974,6 +5612,73 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.23.6", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.6.tgz", + "integrity": "sha512-bvtq8IOvdkLJMhoJBRGPEzU51fMpPDwEhcAHp9xCR05MtbIokQgsnLXrmD1aZm6e7s/3q47H+qdSfAAkR5MkLA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-extensions-langs": { + "version": "4.23.6", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.23.6.tgz", + "integrity": "sha512-VKWbEXmVq3EFYrJPWXH4Ei1f92zxuAg6dOlo8suSmwjmEc0qjNEP5Ss2CUi9LlzuWMGMmZgdKw56I3L71wYOog==", + "dependencies": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.2.0", + "@codemirror/lang-html": "^6.4.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.1", + "@codemirror/lang-lezer": "^6.0.0", + "@codemirror/lang-liquid": "^6.0.1", + "@codemirror/lang-markdown": "^6.1.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.1.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.1", + "@codemirror/lang-sql": "^6.4.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/language-data": ">=6.0.0", + "@codemirror/legacy-modes": ">=6.0.0", + "@nextjournal/lang-clojure": "^1.0.0", + "@replit/codemirror-lang-csharp": "^6.1.0", + "@replit/codemirror-lang-nix": "^6.0.1", + "@replit/codemirror-lang-solidity": "^6.0.1", + "@replit/codemirror-lang-svelte": "^6.0.0", + "codemirror-lang-mermaid": "^0.5.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language-data": ">=6.0.0", + "@codemirror/legacy-modes": ">=6.0.0" + } + }, "node_modules/@uiw/copy-to-clipboard": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.17.tgz", @@ -4982,6 +5687,31 @@ "url": "https://jaywcjlove.github.io/#/sponsor" } }, + "node_modules/@uiw/react-codemirror": { + "version": "4.23.6", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.6.tgz", + "integrity": "sha512-caYKGV6TfGLRV1HHD3p0G3FiVzKL1go7wes5XT2nWjB0+dTdyzyb81MKRSacptgZcotujfNO6QXn65uhETRAMw==", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.23.6", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@uiw/react-markdown-preview": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/@uiw/react-markdown-preview/-/react-markdown-preview-5.1.3.tgz", @@ -5477,6 +6207,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/bcp-47-match": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", @@ -5554,6 +6303,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5731,6 +6503,30 @@ "node": ">= 0.12.0" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/codemirror-lang-mermaid": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/codemirror-lang-mermaid/-/codemirror-lang-mermaid-0.5.0.tgz", + "integrity": "sha512-Taw/2gPCyNArQJCxIP/HSUif+3zrvD+6Ugt7KJZ2dUKou/8r3ZhcfG8krNTZfV2iu8AuGnymKuo7bLPFyqsh/A==", + "dependencies": { + "@codemirror/language": "^6.9.0", + "@lezer/highlight": "^1.1.6", + "@lezer/lr": "^1.3.10" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -5915,6 +6711,11 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6249,6 +7050,11 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/err-code": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6923,6 +7729,11 @@ "node": ">=6.9.0" } }, + "node_modules/get-browser-rtc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz", + "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7398,6 +8209,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7477,7 +8307,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/inline-style-parser": { @@ -7640,6 +8469,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -9604,6 +10442,26 @@ "node": ">= 0.8.0" } }, + "node_modules/lib0": { + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", + "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -11177,7 +12035,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -11193,6 +12050,14 @@ } ] }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -11557,6 +12422,19 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -12068,6 +12946,25 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -12132,6 +13029,34 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-peer": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.11.1.tgz", + "integrity": "sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "buffer": "^6.0.3", + "debug": "^4.3.2", + "err-code": "^3.0.1", + "get-browser-rtc": "^1.1.0", + "queue-microtask": "^1.2.3", + "randombytes": "^2.1.0", + "readable-stream": "^3.6.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -12260,6 +13185,14 @@ "node": ">=8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -12359,6 +13292,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "node_modules/style-to-object": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", @@ -12839,6 +13777,11 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -12978,6 +13921,11 @@ "vite": "^2.6.0 || 3 || 4 || 5" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -13160,7 +14108,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -13203,6 +14151,68 @@ "node": ">=0.4.0" } }, + "node_modules/y-codemirror.next": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/y-codemirror.next/-/y-codemirror.next-0.3.5.tgz", + "integrity": "sha512-VluNu3e5HfEXybnypnsGwKAj+fKLd4iAnR7JuX1Sfyydmn1jCBS5wwEL/uS04Ch2ib0DnMAOF6ZRR/8kK3wyGw==", + "dependencies": { + "lib0": "^0.2.42" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "yjs": "^13.5.6" + } + }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-webrtc": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/y-webrtc/-/y-webrtc-10.3.0.tgz", + "integrity": "sha512-KalJr7dCgUgyVFxoG3CQYbpS0O2qybegD0vI4bYnYHI0MOwoVbucED3RZ5f2o1a5HZb1qEssUKS0H/Upc6p1lA==", + "dependencies": { + "lib0": "^0.2.42", + "simple-peer": "^9.11.0", + "y-protocols": "^1.0.6" + }, + "bin": { + "y-webrtc-signaling": "bin/server.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "optionalDependencies": { + "ws": "^8.14.2" + }, + "peerDependencies": { + "yjs": "^13.6.8" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -13255,6 +14265,22 @@ "node": ">=12" } }, + "node_modules/yjs": { + "version": "13.6.20", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.20.tgz", + "integrity": "sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==", + "dependencies": { + "lib0": "^0.2.98" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7f0aba9259..8629dd89f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,13 +15,19 @@ "@babel/preset-env": "^7.25.4", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", + "@codemirror/collab": "^6.1.1", + "@codemirror/lang-javascript": "^6.2.2", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", "@mui/icons-material": "^6.1.0", "@mui/material": "^6.1.0", + "@uiw/codemirror-extensions-basic-setup": "^4.23.6", + "@uiw/codemirror-extensions-langs": "^4.23.6", + "@uiw/react-codemirror": "^4.23.6", "@uiw/react-md-editor": "^4.0.4", "axios": "^1.7.7", + "codemirror": "^6.0.1", "history": "^5.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -30,7 +36,10 @@ "react-router-dom": "^6.3.0", "react-toastify": "^10.0.5", "socket.io-client": "^4.8.0", - "vite-plugin-svgr": "^4.2.0" + "vite-plugin-svgr": "^4.2.0", + "y-codemirror.next": "^0.3.5", + "y-webrtc": "^10.3.0", + "yjs": "^13.6.20" }, "devDependencies": { "@eslint/js": "^9.9.0", diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx new file mode 100644 index 0000000000..ce8c54ca0c --- /dev/null +++ b/frontend/src/components/CodeEditor/index.tsx @@ -0,0 +1,93 @@ +import CodeMirror from "@uiw/react-codemirror"; +import { langs } from "@uiw/codemirror-extensions-langs"; +import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; +import { useEffect, useState } from "react"; +import { + collabSocket, + getDocument, + peerExtension, +} from "../../utils/collabSocket"; +import Loader from "../Loader"; +import { cursorExtension } from "../../utils/collabCursor"; + +interface CodeEditorProps { + username: string; +} + +type EditorState = { + connected: boolean; + version: number | null; + doc: string | null; +}; + +const CodeEditor: React.FC = (props) => { + const { username } = props; + + const [editorState, setEditorState] = useState({ + connected: false, + version: null, + doc: null, + }); + + useEffect(() => { + const fetchDocument = async () => { + try { + const { version, doc } = await getDocument(); + setEditorState((prevState) => ({ + ...prevState, + version: version, + doc: doc.toString(), + })); + + collabSocket.on("connect", () => { + setEditorState((prevState) => ({ + ...prevState, + connected: true, + })); + }); + + collabSocket.on("disconnect", () => { + setEditorState((prevState) => ({ + ...prevState, + connected: false, + })); + }); + } catch (error) { + console.error("Error fetching document: ", error); + } + }; + + fetchDocument(); + + return () => { + collabSocket.off("connect"); + collabSocket.off("disconnect"); + collabSocket.off("pullUpdateResponse"); + collabSocket.off("pushUpdateResponse"); + collabSocket.off("getDocumentResponse"); + }; + }, []); + + if (editorState.version === null || editorState.doc === null) { + return ; + } + + return ( + + ); +}; + +export default CodeEditor; diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 3bc93e2170..18e9018f87 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -6,13 +6,14 @@ import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; import { useEffect } from "react"; import Loader from "../../components/Loader"; import ServerError from "../../components/ServerError"; +import CodeEditor from "../../components/CodeEditor"; const CollabSandbox: React.FC = () => { const match = useMatch(); if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { stopMatch, verifyMatchStatus, partner, loading } = match; + const { stopMatch, verifyMatchStatus, matchUser, partner, loading } = match; useEffect(() => { verifyMatchStatus(); @@ -23,7 +24,7 @@ const CollabSandbox: React.FC = () => { return ; } - if (!partner) { + if (!matchUser || !partner) { return ( { Successfully matched! + diff --git a/frontend/src/utils/collabCursor.ts b/frontend/src/utils/collabCursor.ts new file mode 100644 index 0000000000..81aadaa956 --- /dev/null +++ b/frontend/src/utils/collabCursor.ts @@ -0,0 +1,214 @@ +import { + EditorView, + Decoration, + DecorationSet, + WidgetType, +} from "@codemirror/view"; +import { StateField, StateEffect } from "@codemirror/state"; + +export interface Cursor { + id: string; + from: number; + to: number; +} + +export interface Cursors { + cursors: Cursor[]; +} + +class TooltipWidget extends WidgetType { + private name = "John"; + private suffix = ""; + + constructor(name: string, color: number) { + super(); + this.suffix = `${(color % 8) + 1}`; + this.name = name; + } + + toDOM() { + const dom = document.createElement("div"); + dom.className = "cm-tooltip-none"; + + const cursor_tooltip = document.createElement("div"); + cursor_tooltip.className = `cm-tooltip-cursor cm-tooltip cm-tooltip-above cm-tooltip-${this.suffix}`; + cursor_tooltip.textContent = this.name; + + const cursor_tooltip_arrow = document.createElement("div"); + cursor_tooltip_arrow.className = "cm-tooltip-arrow"; + + cursor_tooltip.appendChild(cursor_tooltip_arrow); + dom.appendChild(cursor_tooltip); + return dom; + } + + ignoreEvent() { + return false; + } +} + +export const addCursor = StateEffect.define(); +export const removeCursor = StateEffect.define(); + +const cursorsItems = new Map(); + +const cursorField = StateField.define({ + create() { + return Decoration.none; + }, + update(cursors, tr) { + let cursorTransacions = cursors.map(tr.changes); + for (const e of tr.effects) + if (e.is(addCursor)) { + const addUpdates = []; + if (!cursorsItems.has(e.value.id)) + cursorsItems.set(e.value.id, cursorsItems.size); + + if (e.value.from !== e.value.to) { + addUpdates.push( + Decoration.mark({ + class: `cm-highlight-${(cursorsItems.get(e.value.id)! % 8) + 1}`, + id: e.value.id, + }).range(e.value.from, e.value.to) + ); + } + + addUpdates.push( + Decoration.widget({ + widget: new TooltipWidget( + e.value.id, + cursorsItems.get(e.value.id)! + ), + block: false, + id: e.value.id, + }).range(e.value.to, e.value.to) + ); + + cursorTransacions = cursorTransacions.update({ + add: addUpdates, + filter: (_from, _to, value) => { + if (value?.spec?.id === e.value.id) return false; + return true; + }, + }); + } + + return cursorTransacions; + }, + provide: (f) => EditorView.decorations.from(f), +}); + +const cursorBaseTheme = EditorView.baseTheme({ + ".cm-tooltip.cm-tooltip-cursor": { + color: "white", + border: "none", + padding: "2px 7px", + borderRadius: "4px", + position: "absolute", + marginTop: "-40px", + marginLeft: "-14px", + "& .cm-tooltip-arrow:after": { + borderTopColor: "transparent", + }, + zIndex: "1000000", + }, + ".cm-tooltip-none": { + width: "0px", + height: "0px", + display: "inline-block", + }, + ".cm-highlight-1": { + backgroundColor: "#6666BB55", + }, + ".cm-highlight-2": { + backgroundColor: "#F76E6E55", + }, + ".cm-highlight-3": { + backgroundColor: "#0CDA6255", + }, + ".cm-highlight-4": { + backgroundColor: "#0CC5DA55", + }, + ".cm-highlight-5": { + backgroundColor: "#0C51DA55", + }, + ".cm-highlight-6": { + backgroundColor: "#980CDA55", + }, + ".cm-highlight-7": { + backgroundColor: "#DA0CBB55", + }, + ".cm-highlight-8": { + backgroundColor: "#DA800C55", + }, + ".cm-tooltip-1": { + backgroundColor: "#66b !important", + "& .cm-tooltip-arrow:before": { + borderTopColor: "#66b !important", + }, + }, + ".cm-tooltip-2": { + backgroundColor: "#F76E6E !important", + "& .cm-tooltip-arrow:before": { + borderTopColor: "#F76E6E !important", + }, + }, + ".cm-tooltip-3": { + backgroundColor: "#0CDA62 !important", + "& .cm-tooltip-arrow:before": { + borderTopColor: "#0CDA62 !important", + }, + }, + ".cm-tooltip-4": { + backgroundColor: "#0CC5DA !important", + "& .cm-tooltip-arrow:before": { + borderTopColor: "#0CC5DA !important", + }, + }, + ".cm-tooltip-5": { + backgroundColor: "#0C51DA !important", + "& .cm-tooltip-arrow:before": { + borderTopColor: "#0C51DA !important", + }, + }, + ".cm-tooltip-6": { + backgroundColor: "#980CDA !important", + "& .cm-tooltip-arrow:before": { + borderTopColor: "#980CDA !important", + }, + }, + ".cm-tooltip-7": { + backgroundColor: "#DA0CBB !important", + "& .cm-tooltip-arrow:before": { + borderTopColor: "#DA0CBB !important", + }, + }, + ".cm-tooltip-8": { + backgroundColor: "#DA800C !important", + "& .cm-tooltip-arrow:before": { + borderTopColor: "#DA800C !important", + }, + }, +}); + +export const cursorExtension = (id: string = "") => { + return [ + cursorField, + cursorBaseTheme, + EditorView.updateListener.of((update) => { + update.transactions.forEach((e) => { + if (e.selection) { + const cursor: Cursor = { + id, + from: e.selection.ranges[0].from, + to: e.selection.ranges[0].to, + }; + + update.view.dispatch({ + effects: addCursor.of(cursor), + }); + } + }); + }), + ]; +}; diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts new file mode 100644 index 0000000000..8d366f0516 --- /dev/null +++ b/frontend/src/utils/collabSocket.ts @@ -0,0 +1,143 @@ +import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view"; +import { Text, ChangeSet, StateEffect } from "@codemirror/state"; +import { + Update, + receiveUpdates, + sendableUpdates, + collab, + getSyncedVersion, +} from "@codemirror/collab"; +import { io } from "socket.io-client"; +import { addCursor, Cursor, removeCursor } from "./collabCursor"; + +export const collabSocket = io("http://localhost:3003"); + +const pushUpdates = ( + version: number, + fullUpdates: readonly Update[] +): Promise => { + const updates = fullUpdates.map((u) => ({ + clientID: u.clientID, + changes: u.changes.toJSON(), + effects: u.effects, // cursor + })); + + return new Promise(function (resolve) { + collabSocket.emit("pushUpdates", version, JSON.stringify(updates)); + + collabSocket.once("pushUpdateResponse", (status: boolean) => { + resolve(status); + }); + }); +}; + +const pullUpdates = (version: number): Promise => { + return new Promise(function (resolve) { + collabSocket.emit("pullUpdates", version); + + collabSocket.once("pullUpdateResponse", (updates: any) => { + resolve(JSON.parse(updates)); + }); + }).then((updates: any) => + // updates.map((u: any) => ({ + // changes: ChangeSet.fromJSON(u.changes), + // clientID: u.clientID, + // })) + + updates.map((u: any) => { + const effects: StateEffect[] = []; + if (u.effects?.length) { + u.effects.forEach((effect: StateEffect) => { + if (effect.value?.id && effect.value?.from) { + const cursor: Cursor = { + id: effect.value.id, + from: effect.value.from, + to: effect.value.to, + }; + effects.push(addCursor.of(cursor)); + } else if (effect.value?.id) { + const cursorId = effect.value.id; + effects.push(removeCursor.of(cursorId)); + } + }); + } + + return { + changes: ChangeSet.fromJSON(u.changes), + clientID: u.clientID, + effects: effects, + }; + }) + ); +}; + +export const getDocument = (): Promise<{ version: number; doc: Text }> => { + return new Promise(function (resolve) { + collabSocket.emit("getDocument"); + + collabSocket.once("getDocumentResponse", (version: number, doc: string) => { + resolve({ + version: version, + doc: Text.of(doc.split("\n")), + }); + }); + }); +}; + +export const peerExtension = (startVersion: number, id?: string) => { + const plugin = ViewPlugin.fromClass( + class { + private pushing = false; + private done = false; + + constructor(private view: EditorView) { + this.pull(); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.transactions.length) { + // cursor + // if (update.docChanged) { + this.push(); + } + } + + async push() { + const updates = sendableUpdates(this.view.state); + if (this.pushing || !updates.length) { + return; + } + this.pushing = true; + const version = getSyncedVersion(this.view.state); + await pushUpdates(version, updates); + this.pushing = false; + if (sendableUpdates(this.view.state).length) { + setTimeout(() => this.push(), 100); + } + } + + async pull() { + while (!this.done) { + const version = getSyncedVersion(this.view.state); + const updates = await pullUpdates(version); + this.view.dispatch(receiveUpdates(this.view.state, updates)); + } + } + + destroy() { + this.done = true; + } + } + ); + + // return [collab({ startVersion }), plugin]; + return [ + collab({ + startVersion, + clientID: id, + sharedEffects: (tr) => + tr.effects.filter((e) => e.is(addCursor) || e.is(removeCursor)), + }), + plugin, + ]; +}; From d5bcc2ea59d589dc1bd7cb3eafc8182e6151c2f4 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 27 Oct 2024 00:58:08 +0800 Subject: [PATCH 011/192] Fetch random question and display it on the frontend --- backend/matching-service/.env.sample | 2 + backend/matching-service/package-lock.json | 44 ++++++++++++++++--- backend/matching-service/package.json | 1 + .../src/handlers/matchHandler.ts | 15 ++++++- .../src/handlers/websocketHandler.ts | 15 ++++++- backend/matching-service/src/utils/api.ts | 12 +++++ .../matching-service/src/utils/mq_utils.ts | 3 +- frontend/src/components/Navbar/index.tsx | 10 ++++- frontend/src/contexts/MatchContext.tsx | 7 ++- .../src/pages/CollabSandbox/index.module.css | 5 +++ frontend/src/pages/CollabSandbox/index.tsx | 44 ++++++++++++++++--- 11 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 backend/matching-service/src/utils/api.ts diff --git a/backend/matching-service/.env.sample b/backend/matching-service/.env.sample index 4307b9473f..1cb58137db 100644 --- a/backend/matching-service/.env.sample +++ b/backend/matching-service/.env.sample @@ -12,3 +12,5 @@ RABBITMQ_ADDR=amqp://localhost:5672 #comment out if use case is (2) RABBITMQ_DEFAULT_USER=admin #comment out if use case is (1) RABBITMQ_DEFAULT_PASS=password #comment out if use case is (1) RABBITMQ_ADDR=amqp://admin:password@rabbitmq:5672 #comment out if use case is (1) + +QUESTION_SERVICE_URL=http://question-service:3000/api/questions diff --git a/backend/matching-service/package-lock.json b/backend/matching-service/package-lock.json index 1c23f4e4f7..ce7092cf4d 100644 --- a/backend/matching-service/package-lock.json +++ b/backend/matching-service/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "amqplib": "^0.10.4", + "axios": "^1.7.7", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", @@ -2654,8 +2655,18 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "29.7.0", @@ -3053,7 +3064,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3254,7 +3264,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -3982,11 +3991,30 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -5832,6 +5860,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/backend/matching-service/package.json b/backend/matching-service/package.json index eff673d2ff..dbd9db6a68 100644 --- a/backend/matching-service/package.json +++ b/backend/matching-service/package.json @@ -14,6 +14,7 @@ "license": "ISC", "dependencies": { "amqplib": "^0.10.4", + "axios": "^1.7.7", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", diff --git a/backend/matching-service/src/handlers/matchHandler.ts b/backend/matching-service/src/handlers/matchHandler.ts index e80b1748fb..459d981087 100644 --- a/backend/matching-service/src/handlers/matchHandler.ts +++ b/backend/matching-service/src/handlers/matchHandler.ts @@ -6,6 +6,9 @@ interface Match { matchUser1: MatchUser; matchUser2: MatchUser; accepted: boolean; + complexity: string; + category: string; + language: string; } export interface MatchUser { @@ -53,7 +56,10 @@ export const sendMatchRequest = async ( export const createMatch = ( requestItem1: MatchRequestItem, - requestItem2: MatchRequestItem + requestItem2: MatchRequestItem, + complexity: string, + category: string, + language: string ) => { const matchId = uuidv4(); const matchUser1 = requestItem1.user; @@ -63,6 +69,9 @@ export const createMatch = ( matchUser1: matchUser1, matchUser2: matchUser2, accepted: false, + complexity, + category, + language, }); sendMatchFound(matchId, matchUser1, matchUser2); @@ -103,3 +112,7 @@ export const getMatchByUid = ( } return null; }; + +export const getMatchById = (matchId: string): Match | undefined => { + return matches.get(matchId); +}; diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index 4f705777a9..52c31f354a 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -7,9 +7,11 @@ import { getMatchIdByUid, MatchUser, getMatchByUid, + getMatchById, } from "./matchHandler"; import { io } from "../../server"; import { v4 as uuidv4 } from "uuid"; +import { questionService } from "../utils/api"; enum MatchEvents { // Receive @@ -121,7 +123,18 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { socket.on(MatchEvents.MATCH_ACCEPT_REQUEST, (matchId: string) => { const partnerAccepted = handleMatchAccept(matchId); if (partnerAccepted) { - io.to(matchId).emit(MatchEvents.MATCH_SUCCESSFUL); + const match = getMatchById(matchId); + if (!match) { + return; + } + + const { complexity, category } = match; + questionService + .get("/random", { params: { complexity, category } }) + .then((res) => { + const { id } = res.data.question; + io.to(matchId).emit(MatchEvents.MATCH_SUCCESSFUL, id); + }); } }); diff --git a/backend/matching-service/src/utils/api.ts b/backend/matching-service/src/utils/api.ts new file mode 100644 index 0000000000..76c414befc --- /dev/null +++ b/backend/matching-service/src/utils/api.ts @@ -0,0 +1,12 @@ +import axios from "axios"; + +const QUESTION_SERVICE_URL = + process.env.QUESTION_SERVICE_URL || + "http://question-service:3000/api/questions"; + +export const questionService = axios.create({ + baseURL: QUESTION_SERVICE_URL, + headers: { + "Content-Type": "application/json", + }, +}); diff --git a/backend/matching-service/src/utils/mq_utils.ts b/backend/matching-service/src/utils/mq_utils.ts index f1082a935e..4421b7c460 100644 --- a/backend/matching-service/src/utils/mq_utils.ts +++ b/backend/matching-service/src/utils/mq_utils.ts @@ -6,6 +6,7 @@ export const matchUsers = (queueName: string, newRequest: string) => { const pendingRequests = getPendingRequests(queueName); const newRequestJson = JSON.parse(newRequest) as MatchRequestItem; const newRequestUid = newRequestJson.user.id; + const [complexity, category, language] = queueName.split("_"); for (const [uid, pendingRequest] of pendingRequests) { if ( @@ -34,7 +35,7 @@ export const matchUsers = (queueName: string, newRequest: string) => { } pendingRequests.delete(uid); - createMatch(pendingRequest, newRequestJson); + createMatch(pendingRequest, newRequestJson, complexity, category, language); return; } pendingRequests.set(newRequestUid, newRequestJson); diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index bdb98a8880..0b3efc42c4 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -83,7 +83,15 @@ const Navbar: React.FC = (props) => { PeerPrep {isCollabPage(path) ? ( - <> + + + ) : !isMatchingPage(path) ? ( {navbarItems diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 8d6743440c..eb64520685 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -86,6 +86,7 @@ type MatchContextType = { partner: MatchUser | null; matchPending: boolean; loading: boolean; + questionId: string | null; }; const requestTimeoutDuration = 5000; @@ -110,6 +111,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [partner, setPartner] = useState(null); const [matchPending, setMatchPending] = useState(false); const [loading, setLoading] = useState(true); + const [questionId, setQuestionId] = useState(null); const navigator = useContext(UNSAFE_NavigationContext).navigator as History; @@ -269,9 +271,10 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const initMatchedListeners = () => { - matchSocket.on(MatchEvents.MATCH_SUCCESSFUL, () => { + matchSocket.on(MatchEvents.MATCH_SUCCESSFUL, (id: string) => { setMatchPending(false); appNavigate(MatchPaths.COLLAB); + setQuestionId(id); }); matchSocket.on(MatchEvents.MATCH_UNSUCCESSFUL, () => { @@ -356,6 +359,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const stopMatch = () => { + setQuestionId(null); switch (location.pathname) { case MatchPaths.TIMEOUT: appNavigate(MatchPaths.HOME); @@ -492,6 +496,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { partner, matchPending, loading, + questionId, }} > {children} diff --git a/frontend/src/pages/CollabSandbox/index.module.css b/frontend/src/pages/CollabSandbox/index.module.css index 806d506cc5..21bdc0bf3b 100644 --- a/frontend/src/pages/CollabSandbox/index.module.css +++ b/frontend/src/pages/CollabSandbox/index.module.css @@ -2,6 +2,11 @@ flex: 1; } +.flex { + display: flex; + flex-direction: column; +} + .center { display: flex; flex-direction: column; diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 3bc93e2170..80663f54ad 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -1,21 +1,33 @@ import AppMargin from "../../components/AppMargin"; -import { Button, Stack, Typography } from "@mui/material"; +import { Box } from "@mui/material"; import classes from "./index.module.css"; import { useMatch } from "../../contexts/MatchContext"; import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; -import { useEffect } from "react"; +import { useEffect, useReducer } from "react"; import Loader from "../../components/Loader"; import ServerError from "../../components/ServerError"; +import reducer, { + getQuestionById, + initialState, +} from "../../reducers/questionReducer"; +import QuestionDetailComponent from "../../components/QuestionDetail"; const CollabSandbox: React.FC = () => { const match = useMatch(); if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { stopMatch, verifyMatchStatus, partner, loading } = match; + const { verifyMatchStatus, partner, loading, questionId } = match; + const [state, dispatch] = useReducer(reducer, initialState); + const { selectedQuestion } = state; useEffect(() => { verifyMatchStatus(); + + if (!questionId) { + return; + } + getQuestionById(questionId, dispatch); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -23,7 +35,7 @@ const CollabSandbox: React.FC = () => { return ; } - if (!partner) { + if (!partner || !questionId || !selectedQuestion) { return ( { } return ( - - + + {/* Successfully matched! - + */} + + ({ flex: 1, marginRight: theme.spacing(2) })}> + + + ({ flex: 1, marginLeft: theme.spacing(2) })}> + + Code editor + Test cases and chat tabs + + + ); }; From ef92749b0c1746020b5b1d5edeff87ce63c04fd7 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 27 Oct 2024 01:23:56 +0800 Subject: [PATCH 012/192] Add QuestionTemplate --- README.md | 2 + backend/code-execution-service/.env.sample | 2 +- backend/code-execution-service/README.md | 23 ++++++ .../controllers/codeExecutionControllers.ts | 65 +++++++++++++-- .../src/utils/oneCompilerApi.ts | 16 +++- backend/code-execution-service/swagger.yml | 63 +++++++++++++++ .../src/controllers/questionController.ts | 81 ++++++++++++++++--- .../src/models/QuestionTemplate.ts | 29 +++++++ frontend/src/reducers/questionReducer.ts | 3 + 9 files changed, 262 insertions(+), 22 deletions(-) create mode 100644 backend/code-execution-service/README.md create mode 100644 backend/question-service/src/models/QuestionTemplate.ts diff --git a/README.md b/README.md index f6218f3d57..7f05cfc064 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,6 @@ docker-compose down - User Service: http://localhost:3001 - Question Service: http://localhost:3000 - Matching Service: http://localhost:3002 +- Collab Service: http://localhost:3003 +- Code Execution Service: http://localhost:3004 - Frontend: http://localhost:5173 diff --git a/backend/code-execution-service/.env.sample b/backend/code-execution-service/.env.sample index 77b50686f8..561d77ee13 100644 --- a/backend/code-execution-service/.env.sample +++ b/backend/code-execution-service/.env.sample @@ -4,5 +4,5 @@ SERVICE_PORT=3004 ORIGINS=http://localhost:5173,http://127.0.0.1:5173 # One Compiler -ONE_COMPILER_URL= +ONE_COMPILER_URL=https://onecompiler-apis.p.rapidapi.com/api/v1/run ONE_COMPILER_KEY= \ No newline at end of file diff --git a/backend/code-execution-service/README.md b/backend/code-execution-service/README.md new file mode 100644 index 0000000000..64c4bd0e0e --- /dev/null +++ b/backend/code-execution-service/README.md @@ -0,0 +1,23 @@ +# Code Execution Service Guide + +## Setting-up Code Execution Service + +1. In the `code-execution-service` directory, create a copy of the `.env.sample` file and name it `.env`. + +2. Sign up for a free OneCompiler API [here](https://rapidapi.com/onecompiler-onecompiler-default/api/onecompiler-apis). + +3. Update `ONE_COMPILER_KEY` in `.env` with the the value of `x-rapidapi-key`. + +## Running Code Execution Service without Docker + +1. Open Command Line/Terminal and navigate into the `code-execution-service` directory. + +2. Run the command: `npm install`. This will install all the necessary dependencies. + +3. Run the command `npm start` to start the Code Execution Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. + +## After running + +1. To view Code Execution Service documentation, go to http://localhost:3004/docs. + +2. Using applications like Postman, you can interact with the Code Execution Service on port 3004. If you wish to change this, please update the `.env` file. diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index 482a5d0b4f..68c0b737b8 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -1,18 +1,69 @@ import { Request, Response } from "express"; import { oneCompilerApi } from "../utils/oneCompilerApi"; +interface CompilerResult { + status: string; + exception: string | null; + stdout: string; + stderr: string | null; + executionTime: number; + stdin: string; +} + export const executeCode = async (req: Request, res: Response) => { - const { language, stdin, files } = req.body; + const { + language, + code, + stdinList, + stdoutList: expectedStdoutList, + } = req.body; + + if (!language || !code || !stdinList || !expectedStdoutList) { + res.status(400).json({ + error: + "Missing required fields: language, code, stdinList, or stdoutList", + }); + } - if (!language || !stdin || !files) { - res - .status(400) - .json({ error: "Missing required fields: language, stdin, or files" }); + if (stdinList.length !== expectedStdoutList.length) { + res.status(400).json({ + error: "The length of stdinList and stdoutList must be the same.", + }); } try { - const response = await oneCompilerApi(language, stdin, files); - res.json(response.data); + const response = await oneCompilerApi(language, stdinList, code); + + const results = (response.data as CompilerResult[]).map((result, index) => { + const { + status, + exception, + stdout: actualStdout, + stderr, + stdin, + executionTime, + } = result; + const expectedStdout = expectedStdoutList[index]; + + return { + status, + exception, + expectedStdout, + actualStdout, + stderr, + stdin, + executionTime, + isMatch: + stderr !== null + ? false + : actualStdout.trim() === expectedStdout.trim(), + }; + }); + + res.status(200).json({ + message: "Code executed successfully", + results, + }); } catch (error) { console.error("Error executing code:", error); res.status(500).json({ error: "Failed to execute code" }); diff --git a/backend/code-execution-service/src/utils/oneCompilerApi.ts b/backend/code-execution-service/src/utils/oneCompilerApi.ts index fa9dd48fb9..3961346637 100644 --- a/backend/code-execution-service/src/utils/oneCompilerApi.ts +++ b/backend/code-execution-service/src/utils/oneCompilerApi.ts @@ -3,11 +3,25 @@ import dotenv from "dotenv"; dotenv.config(); +interface FileType { + name: String; + content: String; +} + export const oneCompilerApi = async ( language: String, stdin: String, - files: String + userCode: String ) => { + let files: FileType[] = []; + if (language === "python") { + files = [{ name: "main.py", content: userCode }]; + } else if (language === "java") { + files = [{ name: "Main.java", content: userCode }]; + } else if (language === "c") { + files = [{ name: "main.c", content: userCode }]; + } + const response = await axios.post( process.env.ONE_COMPILER_URL || "https://onecompiler-apis.p.rapidapi.com/api/v1/run", diff --git a/backend/code-execution-service/swagger.yml b/backend/code-execution-service/swagger.yml index 805be03337..4e3ab0f085 100644 --- a/backend/code-execution-service/swagger.yml +++ b/backend/code-execution-service/swagger.yml @@ -21,3 +21,66 @@ paths: properties: message: type: string + example: "Server is running" + + /api/run: + post: + tags: + - Code Execution + summary: Execute Code + description: Executes code in a specified language with given input and expected output. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + language: + type: string + description: The programming language to execute the code in. + example: "python" + code: + type: string + description: The source code to execute. + example: "name = input()\nage = input()\nprint('Hello ' + name + '. You are ' + age + '?')\n\n" + stdinList: + type: array + description: List of standard input values to pass to the code. + items: + type: string + example: ["Alice\n21", "Peter\n22"] + stdoutList: + type: array + description: Expected standard output values to compare against. + items: + type: string + example: + ["Hello Alice. You are 21?\n", "Hello Peter. You are 22?\n"] + responses: + 200: + description: Execution Result + content: + application/json: + schema: + type: object + 400: + description: Bad Request - Missing Required Fields + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Missing required fields: language, code, stdinList, or stdoutList" + 500: + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: "Failed to execute code" diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 1d01443cbc..39b7b8ce49 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -1,5 +1,8 @@ import { Request, Response } from "express"; import Question, { IQuestion } from "../models/Question.ts"; +import QuestionTemplate, { + IQuestionTemplate, +} from "../models/QuestionTemplate.ts"; import { checkIsExistingQuestion, sortAlphabetically } from "../utils/utils.ts"; import { DUPLICATE_QUESTION_MESSAGE, @@ -25,7 +28,15 @@ export const createQuestion = async ( res: Response, ): Promise => { try { - const { title, description, complexity, category } = req.body; + const { + title, + description, + complexity, + category, + pythonTemplate, + javaTemplate, + cTemplate, + } = req.body; const existingQuestion = await checkIsExistingQuestion(title); if (existingQuestion) { @@ -51,9 +62,18 @@ export const createQuestion = async ( await newQuestion.save(); + const newQuestionTemplate = new QuestionTemplate({ + questionId: newQuestion._id, + pythonTemplate, + javaTemplate, + cTemplate, + }); + + await newQuestionTemplate.save(); + res.status(201).json({ message: QN_CREATED_MESSAGE, - question: formatQuestionResponse(newQuestion), + question: formatQuestionIndivResponse(newQuestion, newQuestionTemplate), }); } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); @@ -80,7 +100,6 @@ export const createImageLink = async ( const uploadPromises = files.map((file) => uploadFileToFirebase(file)); const imageUrls = await Promise.all(uploadPromises); - console.log(imageUrls); return res .status(200) .json({ message: "Images uploaded successfully", imageUrls }); @@ -96,7 +115,11 @@ export const updateQuestion = async ( ): Promise => { try { const { id } = req.params; - const { title, description } = req.body; + + const { pythonTemplate, javaTemplate, cTemplate, ...questionBody } = + req.body; + + const { title, description } = questionBody; if (!id.match(MONGO_OBJ_ID_FORMAT)) { res.status(400).json({ message: MONGO_OBJ_ID_MALFORMED_MESSAGE }); @@ -125,13 +148,26 @@ export const updateQuestion = async ( return; } - const updatedQuestion = await Question.findByIdAndUpdate(id, req.body, { + const updatedQuestion = await Question.findByIdAndUpdate(id, questionBody, { new: true, }); + const updatedQuestionTemplate = await QuestionTemplate.findOneAndUpdate( + { questionId: id }, + { + ...(pythonTemplate !== undefined && { pythonTemplate }), + ...(javaTemplate !== undefined && { javaTemplate }), + ...(cTemplate !== undefined && { cTemplate }), + }, + { new: true }, + ); + res.status(200).json({ message: "Question updated successfully", - question: formatQuestionResponse(updatedQuestion as IQuestion), + question: formatQuestionIndivResponse( + updatedQuestion as IQuestion, + updatedQuestionTemplate as IQuestionTemplate, + ), }); } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); @@ -151,6 +187,8 @@ export const deleteQuestion = async ( } await Question.findByIdAndDelete(id); + await QuestionTemplate.findOneAndDelete({ questionId: id }); + res.status(200).json({ message: QN_DELETED_MESSAGE }); } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); @@ -237,9 +275,16 @@ export const readQuestionIndiv = async ( return; } + const questionTemplate = await QuestionTemplate.findOne({ + questionId: id, + }); + res.status(200).json({ message: QN_RETRIEVED_MESSAGE, - question: formatQuestionResponse(questionDetails), + question: formatQuestionIndivResponse( + questionDetails, + questionTemplate as IQuestionTemplate, + ), }); } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); @@ -251,12 +296,6 @@ export const readCategories = async ( res: Response, ): Promise => { try { - // const uniqueCats = await Question.distinct("category"); - - // res.status(200).json({ - // message: CATEGORIES_RETRIEVED_MESSAGE, - // categories: sortAlphabetically(uniqueCats), - // }); res.status(200).json({ message: CATEGORIES_RETRIEVED_MESSAGE, categories: sortAlphabetically([ @@ -284,3 +323,19 @@ const formatQuestionResponse = (question: IQuestion) => { categories: question.category, }; }; + +const formatQuestionIndivResponse = ( + question: IQuestion, + questionTemplate: IQuestionTemplate, +) => { + return { + id: question._id, + title: question.title, + description: question.description, + complexity: question.complexity, + categories: question.category, + pythonTemplate: questionTemplate.pythonTemplate, + javaTemplate: questionTemplate.javaTemplate, + cTemplate: questionTemplate.cTemplate, + }; +}; diff --git a/backend/question-service/src/models/QuestionTemplate.ts b/backend/question-service/src/models/QuestionTemplate.ts new file mode 100644 index 0000000000..84cbd63097 --- /dev/null +++ b/backend/question-service/src/models/QuestionTemplate.ts @@ -0,0 +1,29 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IQuestionTemplate extends Document { + questionId: Types.ObjectId; + pythonTemplate: string; + javaTemplate: string; + cTemplate: string; +} + +const questionTemplateSchema: Schema = new mongoose.Schema( + { + questionId: { + type: Schema.Types.ObjectId, + ref: "Question", + required: true, + }, + pythonTemplate: { type: String, default: "" }, + javaTemplate: { type: String, default: "" }, + cTemplate: { type: String, default: "" }, + }, + { timestamps: true }, +); + +const Question = mongoose.model( + "QuestionTemplate", + questionTemplateSchema, +); + +export default Question; diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts index 2d462a5642..3cf4f9d7d4 100644 --- a/frontend/src/reducers/questionReducer.ts +++ b/frontend/src/reducers/questionReducer.ts @@ -8,6 +8,9 @@ type QuestionDetail = { description: string; complexity: string; categories: Array; + pythonTemplate?: string; + javaTemplate?: string; + cTemplate?: string; }; type QuestionList = { From 1ed3636a7bb6ef3115569b2e9a2723c1d10fc1b4 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 27 Oct 2024 10:39:58 +0800 Subject: [PATCH 013/192] Update readme --- backend/collab-service/README.md | 12 +++++----- backend/collab-service/docs/image2.png | Bin 20104 -> 21421 bytes .../src/handlers/websocketHandler.ts | 22 +++++++++--------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/backend/collab-service/README.md b/backend/collab-service/README.md index 3c3b8a5b5b..920fdb0141 100644 --- a/backend/collab-service/README.md +++ b/backend/collab-service/README.md @@ -44,9 +44,9 @@ ![image4.png](docs/image4.png) ## Events Available -| Event Name | Description | Parameters | Response Event | -|----------------|-----------------------------------|-------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------| -| **join** | Joins a collaboration room. | `roomId` (string): ID of the room. | **room_full:** Emitted if the room is full (only 2 users allowed).
**connected:** Emitted upon successful connection. | -| **change** | Sends updated code to other user. | `roomId` (string): ID of the room.
`code` (string): Updated code content. | **code_change:** Emitted with the updated code content. | -| **leave** | Leaves the collaboration room. | `roomId` (string): ID of the room. | **left:** Emitted when one user leaves the room, notifying the other user. | -| **disconnect** | Disconnects from the server. | None | **disconnected:** Emitted when one user is disconnected, notifying the other user. | \ No newline at end of file +| Event Name | Description | Parameters | Response Event | +|----------------|-----------------------------------|-------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **join** | Joins a collaboration room. | `roomId` (string): ID of the room. | **room_full:** Notify the user if the room is full (only 2 users allowed).
**connected:** Notify the user if successfully connection.
**new_user_connected:** Notify the other user if a new user joins the room. | +| **change** | Sends updated code to other user. | `roomId` (string): ID of the room.
`code` (string): Updated code content. | **code_change:** Notify the other user with the updated code content. | +| **leave** | Leaves the collaboration room. | `roomId` (string): ID of the room. | **partner_left:** Notify the other user when one leaves the room. | +| **disconnect** | Disconnects from the server. | None | **partner_disconnected:** Notify the other user when one is disconnected. | \ No newline at end of file diff --git a/backend/collab-service/docs/image2.png b/backend/collab-service/docs/image2.png index b8553cca43de3376a6deac200cc2fb734271f242..0c6924d98d03134ba1b4e297be95aba2f09494a1 100644 GIT binary patch literal 21421 zcmdSB1yEdFyCsafyK8U=7Tf}Kkl^kPAxLm{cMSx02=4BUOFBRZE{#iYcbiV$_j~W1 zJ2ms)nLG7YO;=H;HwEX=XYc*2^{llHk;;nF=qMy8P*70lvN8ZwC@7dfC@5${Bm~HJ z?xzMT$9?$NPo%J#1fU4h5wq zAqx;!_b@yHqwA|%6o{R!UVUxTRY>nGryP0vYzr`C82I=thHKDJs)BCO{3>JplL8l- z0wI{EL^jlVA4rX5;|k>syY9ak=Uu06gXAF}l)YUX8BLah*zLoECd71EkYE z^opiSpoUtnaAd1CD~_`KX`U}~G>MOhGn;IV$jz1S#Uu8gRQSmq_|Z8?^d6yHV$5)J zjJnAqFDRrR;jQP>?Q08v5wkuw!Wd;kBM_eq_TPM^wW?)SZlH(h|9UUog5VIH5M+%2&PcF0Q4_uKT~2o@62QFMTT~!bsqpHW(d8>_ZfY9^l8|GZ3C0mjpkxU|ZZ6(xDw4Xmc5bP=D#LFX zRa$=eRFktC-HT)f{J=gj$?xehKH0a@@pXN8pq6&SRsPD`F=~BWtzPnrmC2&y@Bl{% zZupkR$z(~^v6b+}$BFc8#uq%I8_G%RfYU34%0=whJGByOzqNO*w*??95j*M~Oo5nl z14Q+F5Y`K2f8D)*OY{g4mDd&aNvMxl%r8pp=*c8*M!c<~$Eke>R_H64B@a z`J>$#exD%`{Y{Wd&P3H5qHa!5Qh!{_>E!9&0}h<>?{@j&43$249(qfKQ?=v%UL?2x1)y4+*w}o2=fI+&)^LD9Giilr4Bq{S@t3=g!-a z1&~kWPxYQ$lEyyIIuZG|;gxAsVE-g0McJti*c_VVf1K-Y1s%AY_A0Qig^Foy-oBH% z{5%(S$w|$_jsC%PfBm*vVykjFrl025#pChD7t425{(I-B z^TscJTl+1l6Z?%CwYsST*GK;Q)tsn#wu-Zwv)B^f#fsg#f^3W-K_3BjMRcnV?xL(_ z-#9-#pv0V%g!Km256h2qm&0WiM+1A5X7UdC;;9O6u?oWUgEGi@kaF8$Dt72-ic(pP zA`eIOLqrLq3oKCr0|R_hVO8P@jKP)sBdt1jh^PeB&HXzZG;T7Crj&-EbnYQNZM2wb zCFcEZI;?%@;l^TE;qO90iCjgoYC|0_5u^T(7|uEJ!^&6T{p(}S4!bMfb)KJz39|$O zPyC=>A1^ti)vBOQ<*-*}_bp%SkZtj+&Nxi$o^W^>EFyMo$EQ$}FvX7zl;)27L{zz+ zo+^!OSUaf4na&}G0@V;w?Ji39^QxCbS&Ax{sI$+$_T0v5HO* zoNhUpy88*4AxjL?3pw_@+WXeq_cwiK_VI-iwd&@g=2HGw>8BRJ+TEko%}`*Jm=)E=aOU^jqWK)lJ`TYr&SmbRcwWN_y(5mgF_1FN2cETYcx{J=0Q!d{QOkjI z?*Y?qxKs{L&i7bz(SHl0>h3w>AaM<$AeN&Li=Tl1js zX%eyz+4?MUp@|s8Dya9J_ZQ~;GQuxSX<36Sl$oEgE`6jnDerlT(JrP(!+qV7Cf-tJ z;rSO`cYC4iC`5-d*yAK(nj>8fGI(<9D|NZ|G;#Yk+YG(fx@If)f5p`^bQ(*0%COlL zlQU2n)84xWpu78CXV_d9PP9hZsP9cl8|YmOTmso#$$ubBx6ZyueZL`h&vjYr+BE>) zgN=MiU-}uUwRXqdM1XaUKE9!)puz%-67Z8Qc)Yy7W}ue1`isV~_|e?Oo3l^Y)rhOY zMF_U;Y;h4Eb$2Rwsu5|W+MGU>8}VQ61Xo8Ot~z)YpR+#n&fS#xac+$b>${5>z8Y#g zK2T^dKnA_ne^Gz>NqAuAI0n8AJ&69CUhVDYTgW2bdF)}`d@yER!kq78>B{zWC9kyy zRDbb&1;ph(ClNh*-&OH=!>!*i=a+w#UFtQ%<9&Ql4dkt?Bzh^haY5Ao3dDbM)@pC| z>IxNAx>+(!-8J*Rq&?HgrS4{b$Fu*SbFz4kcqZ@+rcd>rJ@Q>0&rNChx)gYmV|{M$ zXj-LRVo5T$eV z!>LdC^M2f&H&-M#mi6ntKU%|iY}G!s6IWZVocTx%^0MJ9DL?wP0y#}1FduAh?Bj07 z-`JX-<-F`@RnI-&_}H5ito>X!W2%ef%X^6fnmYFzWMJ4fFaCHL%EVq$L;slt7(~SX z<$b_zjI1V2>=gmL^>bu5eYrvwoq5e#A?@3BYj#r5z4+0p=O}$7hey`{YGvFb7+Eca z_1#WY4)A!;C`BtIA9%}hX0-UIWV*yl8WVZLSJgiuMO(Rk8N+@hJn@_mGcqzlUsHA< zjE_3piio44`2qhVB-3naiLu)a1_v>kq69xwE_OsWA;{sVG}rhIt@HCMZ4h&3^$9oC z5q1q%@UV%#DSEU2!F2W!{6X3*H3^ zRUNB=yTTQEf6}EBiO3w_-&2`7_#3|f*Ic*pXzY~4JLjm;a!W{ExrHh9%+!l+4bgfc zLO5LO{3r*EZq(KuHa9X~HQo3MO^A+4dU@UJ)Re4V>6%%>^ZG3l7W$i5Y1*K%N<3)a)*IcnqI69xvd2iKbtpCL2CE)Mc2T}8oDN+|&1c`3HRhvetXMvTYbR_x z?FbCIiB9PWz2BQfEFO(cczSD_l*u?A4&5>#>3087zaFw>wBH~$d>HHfti@Z^JbFE`G)@R<>=&jMA1&FKoL{g$F1to_(nwsfq<*7XUzZb^ zXdtjd)3LMaWP4?P36}9X`IKx8{DOgf{4L>)MUYSgxj+cXe4^MC3@Bts>wJU8cvF#c zyg%kNuzYIC>dHUbH1WEFxYi=Wm?SAGrsTo|4E`1?ITp#WOAhmOpWBS{0-F0C)X>xQ zTH1L z`==qL(qX#P9ABJ@hN-6Ua0sw$xhBQl*z8mp&YAlhB&uUxw?-#T}Z1h7=|O`k~5A1*V6NpVQ2#jS_@fDFOI)9z0EqA@CnS| z4M(yXPM?^`Y>yTtXsk6Ds>`{edI+QYDeZ$4e5^jrI+0O$kxDf=Re_YMy9jSq zj-J{n%hZaE8-?G~T@v%cFUS#^d?+VQyv3nok02js(fDyKeH8nm$t!mH2IbS+0figm zMYD~XWd>GD@)>^5@+Wc!?S7y6gpavn@lo}Hb=*0h>LkdwqC;gcv)h{Yv^Jx34; zlHACH0B-k;m(S+3&E>G~KgF%;#qOg@n=KyV#l*$G14MKpJ%-FB#zNnYQemsJwD3G7 zE;S4vAf?$~q1j5kTH!;+uSqR1-CSan>!Os+$pqYo(stUjag3l{$kfb5@DCVJ&Rj*8 zZj<+iVF3o?hl}7rb;nn%M9;Mdv%DBchCY!PcROiFVFnPZ)j${1cO){{O>@2Bqh_H~BsWjal6hn#Hyu}L;bQk_A&1%;P1aUG!x z+rNRB;iZ`bKDS&=(UPpxPYBVOU@QMUJ=E}r%Cra4$Fq~1GX@o#im*)@Z2hg3&aSgC z(O;XlFhVvn$6%f4dOlu0fj;wgP*CjzYOT2f{Pnbe^245<$9j{KYi#nCq{sxHQUjZU zp#sYXB2Ek@qb=imZv3=l^V#EHGeqBI($@jwL4+!v0Cea1gj~D6`CVUr)7?Q2Pk!T; zem7-SBkz}Q9PB2n^&*ic$+*{5+qLLz2$G93sf7A|bQ|PGCcru9Zq%@q=myj{D@6~{0*0F}`91qj$e(m9 zOKjA4A2r4rZ8dyqY?U9Q=3BZe~W7ibXpZY*gyq z8j!?}4VhRS-_RT$)SUc~v{LgI8;-WDF8DMHi(aGAZkQkWEK`y^MECe&@pun(=jDS5IOQK5-PA+& z`VBr3lsx%1cfyLHI_I#6SCvnU+E*ASW%3_(+L;vOP1VtxBD7_S{qX6)mLuqdU(Ss6 zY$-QN@c**DS7M#<2HV*~4_XsXeJ#&Y%p_(=Bwl{_qw8c%33AwYtiA2~x1?Y8j4J$YUUCJy{UZQLR~>0 zo_~J$plztw*Urqj?A+VvQjUSo!o}gM6WZ=M5HO!B7M}qvlH==$Jk;X3SW05k;2(*5 z;}gKdX`OKSq*$q!9jb-tTli#r1MG=GJTZ#ddYSWh<>0@kE-!S?ztZ(W!n5^gRiTrF zF#L(pM-(&j<*TUnkcW0Pe&D9FQWC>h#*foq-(41O>}`@Jx;td!PQ!LIAbT{Mo!o|I zhoia5ATS76anfbnxlbW{e)`e1A;SIvUWuC#gILo%J-pb&*ikX*lb4S>v1YgGPmfK& z2{aLpE4^x1`;DhA_G;6TQu!~c5kDh zAJ5OH{N;w;-<}W$6Y$I@NVY{$I)G>b0(A46hp^7~szrt~s<@o7xzY-#xl3+03pwQC4=dBZ}Y3cGMAwGB+*>5aD#oA!u0W7HW521l?cXV zTPdbjOC#(LxASAgmRU;788?yOEZgZjAqF%;Qj=4RNb+qwe?QWUX4q05Uhve7^tYqc zQ#tvH%&->Ka1hI5hTxeJgk@=*I5u+5@t^>Bfs%a8Stg2NfnDZkOmc7fA#caG-W}>#$z1ZgV&dsynO?_3e-eXZz2!+m z^NB3y$x7Jk%7&T;+a`*xCC`_LQj4l1k|#w`t){DN1^YTof=xYeZ!@>y@yHf8tDbh| zW^Sd_Y>=?W_hOoCUX?VJooRvbX(2hXu>Clh)F6w% z;uV`yc$ioCEyxv0N&y7;HuI?^Luq#&b9J+Wg61?k`$pHL;Mtpv%J9bwmK-23`oXq< zQsD+OkRoS_a#1Bgbi;^-21R?>5;xoV9{G{L7xx-dBGq`Qek6bCt`5I;Xsai zSpTVEUEMDzH>aDI^4Hc1C)6+b?T|B-(h|YI&hsh7s2NOWfO1$g3?Q28H1@xwR~wi} z$Pc+hQ2)NHOgPd1!SGilLzvIS7+-lb{j$Dbg#* z;U{(2+yDE+!yODt(aPVSHiG0SQ%)J2Vc&g+kZRzTzK@EgQBy>Ff;%(yJwS-iIB+41 z4s}K16DID@l#4?V!r5rk)>6+Sd-kwnt+p*U;!7G*Tec#Sn(F8CZBze#$A4 z_mJvaju#2tiz=<&IW8)}qad==Yleg7{L>k}+E1&FS;?Cax-@_K!1>8k_&zsSCMPE+ zZh2XMMj8Ns-ZZlCxG%#YyS$J6SdQYk=ec2Ywv@u+w>163&0kt{O!EF4vp})N;>jFT zlMhcN(*&0WxmQRpmp{d4zXB;6-D*ns_&hm{eZxt0Q!MK^WR>0+T8ZX^xQr~+|1$*USbi@ApD@S&Be`2Q`-PkDK zk{^~4$Y*ndrf=gnOP?OBSs$N1sHoA<6!6}j3;kR=xY<>`dZ6{MAPI|1y-_lVUoqS! zHB#_HpO`M#n&Bc7F1#<(Ln@~YJf^j`Hs(f`eyCQEJeD-{kN4@A^i8gfC!Wexcvl(G z)&0F@baI&EOluK$qvj2M`clU8+Jn;Oo;xNlnaZW+(>+<|>6v2)_&0u$fMF-d0HYfv zD%b31TC);oTAC5wwZKyuDr}X2{ei@uxR#wp`90xNpFIsWWVqmjW5hP2BZW zE!)s7DHgSmC^(F{2B=Q3r_q z=Y&cz+KtNFd4Yo15n#~W1Nk1UALii1dWR>Y)x{wZd{Q1tAOq07XZ_TVd{|a#jXT4! ze`e!_z9dAXB+9GZlbFKzbaf#A?K4VelQjBa(fq;2JB2MHCrA_ZwWZc^ zn1al$eHgNhn|8AvfD8gYUmFfqNK1Fz}Tl7TR@1T=N2gz>e~khk7iYmyuAP z?>DV%oa-zU-VFPP{KU`W z$4<0X@KdkY^V$2(GkvH2`0pd`L~Q?iIKv z=M153{(bFtrJ0Hu2b@r{nBN-AhE?kB(S7m})ErRCvF5v1B7iHt{;D$%EGpDW@Hc!W zAxQQHbGMKyb&*U{H!=E-`+jR{{1R>L?b_fT91-!z^wv+>?{f%~dnD!T$>2#o380s$ z)Kq#5v7*jPVM=Q>?nBi9daOGifq`BHPk_Lh7q>1bKy+*(^L9<_ydfo?xtSeiN2Q(~ zQKk6}ssIZZq<5xHfKooV7QMk)@YQ)1QpdXlM0@qd{Gv5q6mmwbkIA0m8XF&ZGMhkU zy4=Qxv@*m6UW^~%HIRfmy-Ay&3il4C7QJ{h_e_HB1U*lZ@emr?wo~F?qhd`TAO73LWar6a>Fhc{Nl~<1XR}p(PW8i!{IaC3CxZH6=bJP z#9s+XfC>MGw6nLb|M!vZe+w4>fdna6$d#~F+-FRYIuGkX>Xg5SuF}Hf@(@h?ui244 zrbv)8V|hMo)|fn9>K!iY#Y;*nu3ZxI6ySPYLrFXs1lS7#|{DZ*a zZ^MAg8&CWWw;?1O{;&^50hedNLD(ACwpAHjU@1;y_;f3y>$bBvM_d`v!)!#r_#qe`8Mm0$+v+v*{&df^ zEl6r>TB0?2F5kiWReg(<2;?@Yo#WL4Vb;nQCO;v(m5oc?Hur4PZ}ZvjCW3jY#`(%V zpYXYZnC>0COouKyJ!H91d7|fujnD?1PuH-U3+Nc0&z&=Iz)K^!2(IzdKZ|XLnAM>3 zo)p?R&0}*Kc4n1K*X}Gb(+&${>6T1QR?7Fg9G@MXC)W^yOTge9PXRscW_`m7SIC(S zlBCxk9dRQ%5o)_=1I9+rodnx_D9{hPgJ|GpznJ$lR$_%tDR6*I#mDL0QCyBkB&{XT zq`nr>T#dsGyeguxm!9gGwM#E2OXE6MY5g9gA|^MOmN~$6JT5+1&Go%Q1bS5MGpDr& zn*842zWOIpw=G)N_qN!uCid1eJZqJ}LF~XsoQltq0l|Z5&u^(sohy{DQgM=#{lnNM-ci2< zwlPixrLM^lk7i&#M8Mc$TQsYGdRx5yuISlGi2kt+g*1vc&-wyqper@F&!F7L27JZ)R8Y2)90Jx-0L>GSu zu9<`3-Ax}iy2|Up6{dDDMj|OBTjMtl_S|Yx2ot{yMd$3Xu9>{|VJeJ75ii|`)YW}O zbD@Cj$%Q>e4>%^@86gZZTRp%_P&l zGbVu4{@)u?Lu+nCx_6Wz&?SWR#4{udxGx^a;Gu+Qlos*sL^S%x9BV{m9KgI0xomJ< z``Ma)1C5;VqL72eY8*wLKLH(tPM>C%iTE%tU-UGONE;c!_;J~HbDL;>RjkBxyKv2? zYe*?2X`8OiM+b91r+u&?xJfG2o5aMnkWJp`+iigzSpAi-iH5JjSA4(7N{^W-63^Iz zb!mk>006JZTypHf=;{5HtAcbcIV+pr>HDxAyGp%~9?CC1Ua~B6^CYi%`+l@Rasv?& zx{V%3qCjD>c}Y=Z052Td5_Ixd&j_vmIXxC+3-038zB!v!{+{a*x?gzwhwn4?+zBBy zheIn<{-OOr0m3|4dY!B66S#J*djSbw>5y_ z@^IY_QJEK(_5|j`?G)kPv>s)jAq?wxjosz=-)UU_Lpg7{9l-kCCG4-<&h!6i7yfT_ z5gWLYkOm=WWQo!r{h(ykba%yzOwb9YWTF&1`$q#J*5V0_K6fiSPPjo-hkd7hEVYR7 z@16w(hU;;g;AA+kpJ5v7g>?P}rYN3rfTI-mPXULvJ6rVCe(0s1nd7;M>m=Kh(hsw# zK8%)oeHdCetZ1^Ub;St2N1ye-Y|{L&QI(%2c7{@w+0Opwu0rk9KzV3*7zqn_a)e3g zmxg718qP8Ea2hl8F;ttNDuwOj+!A)AaP0AXf8`=eS5Hfu*rjlVgRRm+tpxqR0#%(s z4*md@yyc;zn34+m0xCIa8n8P7QmIFcKqWnR z`qZb%xN)=?XHMnNV{c;Y*BLX(b{&*%yjpUNBJc+6G2POxQb!>R@emrT`5 z;FV-1vwQPe17^dT&ki;A`@vrcuUMIMgYYKP&tpoZRY*3r%%#6^v@8hP zW^4l7bTODUFkI|Fy^<6BiC6iS3z?jE(ZXA8(zw_vd^AhYmx!K(4*$T8IFd`>y%z>r zye0LZZzRkQ5YojVj=IPfxWZpesZ}97oaB2S7Iq1^1aZ?N2R+OR=cwDetz=$2Tlb;+ zq~j$`kK7KgMWgrx$)|)b5<$eNaJI|HH)p)=1~EY0>BiR$^&F$RQ78hr;CCZz=>e6} zV;o+ufkatB_%t4hQJPO|wS>C`hu!Z+E_K6Cb1kVWj6_A3=^nC58NUTPDmEOQItT*L zBO^*P{|9)1qy7;n`2zQyQ>EN!&my2MSNy<*thQJ9T$c@tmy5(bme>zUfxo+B2xQwUNaWP=(DDz>v%^@H62cF>%pq zdFMo~oDyq(a;KOUx(rsFZ%i(4mUL-2`oUf~=n5$}pZ^WG#&`l1OKzGREu9;=S8~7K zSkTxgrp(!3YgRJ7{f4En=3Vk|55z!G%9yq7*3o_zgO>qzbFMVEkVB*o?}T{g3&aI1 zmwz`UXyWRP(6i)Yb^-jR(u2}&N4j07rGWY7}yu(YZ-AO`GpKYdCljz$t7W)9my73ugIo3Tx zNeagTgG6nF4x4zEwIv{YI~-EE#YQ0b7G^2vKNAe2q)dh!Lz;`xU#~OrEHl`t=F0d0 zmP~7}!{DL_T^1!ys&q~~W9aD6`J#p~|J;hT5J#LQ+`cEfQO()oVr;#5A_@IKNHb7QZ`yZ~7It=G zVP+DkiDWM&v_-9r6x3^1)q9}?Y7txw~ zPS9@sf%;xY!P|XeM#eO#B{O<}loSn@Ahh!hM2ueezy`O`2VnKO(Fy7PATHlXW-npw z$e#kth-~Gp?mxiON&V82_h_PIATQ&`jW@AlS!O|;m~0UAj{2^4AmXebhUqI(ejA8m z)L9a-AmRK#p5Kp;pm^O`g`V30_)54#6x~CoDmd-+IkQ|YKAAW>u8NI%V{D-Q) zr_dU05Bm4jRIn1(*;_osFvvvn!){l5o(ZWS#3)QW`!90f5IuL?zle!Z*6nQnH@weC zgYp0TCe(#HRHysaHWA+jm%g)wk-n1&JDG?ZiV(0|dT-Xg_PMY|g9$fBf|@IK2m{O^IUe$G=YW5+i;b97qOO!W{kuCWuC7pf0ssP)3}Js^a{c$z z;IYXEMIJ2>uZKC2dpRVF8IpJ{qCIEK1KPnPN<(dz85xZ@)px#1Wj_!Ht2ua zV;H3SJ|Z-;922uGNzFO+@~AV`+I($bi}an)t;d<#=AA+-vnP_q%bc>kE%ot2Hl>q` zdOfzc0K8UnZjNWF8fe=SBxC3L&Md|AKb_M%q)L|204p8ps3`>*8QFK|pXdi($xAy{ zF;iuii|eGkL!%6yW*Q~pd4rHrQia|-TDQl?A)*^GloZIj1-1xcszAfwfh%E7bPk5U zn<)osoFi#K`n5+#_3#3;nnF#5ecFAWXxZtK0!JN*KeQ#CA;4j=Rp^iokG#1I;zVzuvecWZow>e8&iVAl z1@Chv(DL6A8CJ9R-KFV2`vf5r$Ga+c-fPyj?vN-3CpUH45xL-*l6T=6GTVyHkl8+` zPZ&Ry->vadUPI2A>ixq$xc_D`$_^8+%~?O8IW&L4q%O3>;Z^@6E<1^!u2B`_Ii>#H zcPU#iW=Aj4oK5UnJp%KO9i)@A~ZI4`F%LVmX)Sni%SQ^)-C|t>W}&kLs2-xG*vz zD9{#Zk#ftRCwu+h8eJE`;`G?#V?(L5VLE71_rBK;*_wE-d1!3JQTjwug^9*Svc!;Q z>rvc){jwZlgAl0~TKD;~hb_r@|2nb>T@;mWNm96nSyi(36M}(zw*^3ER$QUOHEQT& z9)dnm`1d}X?-8l6S+(6cu$wgm5T>>e>Ye?;!sK%k9=;)y!(*k3hh~S!^@cv)YGSB- z4RT+Sma*LAE5K+D_^WmsC-xp@rpaZcnG6lr?yCY1gEFn#H@|~op9DO}AT7M{1EC@Q zL1x%xZg>SnUDo7gf_4XaM-z=`W$twj@j!iMx22A~*7}0plJ+`;$AQy=-Zga2nmC}; z?+_)v@VUb^rEd3UEcytU(}>7lVPWq@{MG>=7T}+ro(v}pYi1$Oko-=-35kaWV`rkV zhb#Z=OJ}PLY7~!dK31CaaDJ;q&a6w`xjlZGiyqE*zI2sljj(V*b-Q}%?HzvX!1i%| zUR+SWvk50%&p9Y`L{BR+BkbY}2HAZH5=#Z8m8y|kl<r{zN77!wr^?IUX;*$*L@>e(xb6)#^8`X-5>NuTn* zoS8f|-h7Xlm(f`Pg-Q`EwY$8lhJBxyy}@I2mT^^2K2Gk&He$U*-F)YU^e!SL1hOK6 zAq}tCh#QnpKX>+DvVeqf2PD7nCA9Ki0iQu%Jb5IbAB^cLbNG=q-8VT^hkj*j^qMaC zp`Y*P$xYK-aqKuBXkk#g`Zx`9QheR-JNdd7NmscNKo7RS00%!oAdp72Q zyC+`3y{?Ik8*W@-I}HAe!fakyg2RA@kV)sPP4~I$r!0?98|fDam(`ueaGUR3rKBl< zN`+wY%nT2N&-`mkPKik5^~NLe8?0rG}!7(c3Y>gHf{dW$k-7{NtkCldCixJt( zsGpKKuU_|UDc7+9Yx#JwxjkTtb)piwN?RS5C>l-I!JO#m;^C{D3ZD>NgY;NCOm#4# zd@ybSfZbdl)#09J12KFSN_Yl{28R6`D_`d-q3w0IkL|6^Tfu9)stFWh!~@KWJkmd_ z5S_CcW`ZwCfb-b01!4p(`b!?6Cd;3a25JGzTzzvy=767B_-PJQq$#BG9AI9|>$-HA z3&CUpo-6!HB4s4GnDU8Jw0S?3fXYd0Vy%g{z2EedpKC`&L>EK0_$1G4{WKBdqcQa6u+4gjQ(a{vV-Kn8sgvgI0 zVR;sRQR*$F6*!t2=;V?BI)t~rf2&aE#NBO!4^P*^>nRY~Wt+*^5U~{VFB1U}CVnSu zbjQzk_a=VIaymls*Frz|5B*<(|9)D+{^zFvg43}i(GcC7hQ{b$l=%OIPX9kC&bc2e z{Oyn2mv;P#BA^&QV+8zvD0a+)RHc9UG|=yJU~*pewaRO3s!>j&85l5!eng^D9C?#I;n!D-_s7}b*N(=ze~S|#@rP4CU6f4902)(x0O_~V8^wOYHm zWI`){-u!2=)LcFYz1~#%3iGF`5aXP*pY2M-R>P_*>Y_0c-Vtl5aO3iC*yC7 zIL^xLtRvvHXEGWMR>;?X@uA?o5PGP-d*cSu`U(t06md6AezaIJ3e0aml5AiENUe?r@4!4 zwo*tOTV?0Hx>smJrAB6QQH!BnFBN-dfv(hx8Ch!5hFuTY z##HdxeGlPZuHrZlzU#?Z~B%2;f-ADqvQ_hx`dSbn z9i!V2bza}Hq;L)UAB``T^ZgVE%1kVHcJD7YNtepj959asTX{`VrNn|O1ORZbcS&Gx zh%C{DE5iz%Tk0C8|G0f`s1MO2nTDDa^rZAJEJnOYOg`SePRABicC{Y3-AT3Pw^43; zjk%Csws?*^G$t8JvW<;TQP55zAYz#vKi%^oE>!!8UIGzA#Xy2^?)Uy+gOpEyh$CfgqLu|E;w6q>LSJfpYn zzYj{-fF_2seA`!(M42ATo#S!Z{lN*+zA| zwOi4dE!(Ul#mqw~4o~-oe(;hCQA(%e8pksBZvn2K>a|AU!ypaf9Q^BvOgqvG;K3- zl*BnINJ#8oUs0?~R{P{gT~E%<;n^rO9G zbm`@?^2XmhO{i$f-Y z{M-9N4A#eO;Vt`$z|B1JSqrtq3wOxVD;<9-f!!#BE85}em0I1oiUWipl2d~~dt2vs z0}010{ECQ1BCzoAxW?Y!Lq9eHIgODD}if(}LsC zE`M9hRdmKQt*0-e=A$1DiT%aL;TbCrt76=RmeOs^+|ZW}D^)}#L{hAqUxLZs-xNHS zNhjQ&O85|UBY`Nc+v$~m{cOjyET{>P+onZQVir4(Z4$noV4 zE#N6yZn!%8%p2cbAg?DY&0G1HjT;tCYQJBq$A^!s^u|U_N|xmjN9U|u`o}(XL2}(# zXk&ipwtK{5HD4yA4f5Wswa`GxSl0YGV}T91XamG6tbAHa*gxou6BY9KTe{*2YcI@0 z4cz@Q1ue}oH;14ZP%eJfXx=H}MXvdzPaf(bq*4}Ub zP;tiDZ>B0UYJ~1f`2=5Fy{8P(m#9#=29?V;-FPTn28e-6=7}x;S?p#`dHV9577|ET+g&$w@FpcQc%dQh8R3K#u3=D1h9OA!EgHEILFdI|^) z_Vc9%jh=r7rYHq6@k``QK@W2(mHZOj2)Ib@cww!tBwGEc!ck(`ZsuBhQW~f!ks`+2 zv6a{`wEa=~C)u&5M@pI3L@~LmjYYbt?x%E90UjaSQk$xkqmOkMi%yv9A4QDSuWnc? zew#l9dDQWJcbmU0b7OC5sLZ78Z5O9IVUl@Y_`lT1Jz!O-J{Li;W0{NsaVSv|{KBlD zhJ;h!b+~$(sT8$m6r*zqGN=2PbUx51srMgO4CuFI(nv9{+n%WY*R9G!oe#LRo{O10erX8rG8+;Bj!{?}{* zHl~`sD4XbnxNTh5;=BJ=1;&5{me zjiI1GDvy7HJGA@1koz>G2UJY`w7Xu8$dNIX=ogNEXW>@0ij^CQ?ynPfW&(K{ttVlg zpTFYBFp!mOuR#^-c*z3$?P?=E2#rz%BeH3!PA@YbcXrO%_;H4&sFgh%%8S?}=S!&U ztg5=l*PgS6pC+2A`>rF$G+TD^y#+cSh(uf|vVtlFq_$qFsG}eTJ>M(Rpld{<8hG3B zf0U!OvAPJ=Y&$7^5gLMKSd1BVQ=LzAC@%@IF{1?9$M1XQD^Ylc%#b7mozx%O9k59! zuKTU-YGA{kk>|C$*QnnO63$O2kRH4I}N_rsI*55(B)>@Bq$)8TYF9i)iV2Y^9rYy4&?~# zJa8rQFn|u4kXUB%s4OCwe`uI&a#W2z4j!~=FnUb0nsPv5a$5f}w6lgBm^-gBwH5za zaK3q#iu0Rq<@mEKb>A*)|M?3Ern3*8#659ihvfu0Z66}3@)8T$PU6nc+?8Vlsfm=j zm_{XwL?5=+QOKEY%^hg2$#DZ8gBMu=Fzq|~n=Sd^{9A%b2}!sa2Q{}QKoUV5`}oj5 zBRcejDE>h@{9_?{YDoXHiiM=I%kgWeak()i#Fhdhep8e1s&?A3G9u&de`z_>*rO0P z6Q#KS4L#|X=aJlC2IGvcNDI+XCO6+t^6*hl;(DiDkFwB?r+@F2JQ*^#vcEw2?o~ou z!nR0!!e41FC8SP!ffW5Vgu((^Y+`a)5UP}Ul|O;(euUoPR%*iD_n|Q-AM;o8@lTX? zX&ak+Yt-jEsEAHO>BirfB{yYv-1H&LgvDO`uAlx>dg3JgDJap6xAk!vnWdrk!=TOoz#x$3LJX8SILFl7t>xh16wV z^EbJA_Lbjfy#I?iln5+vT)G#y5t0gN0$*1`9Jg$v`uv@{8o#gi?Zt%Ps z-%k5i@@4mgYMufjZXiYd_8yDe&!-)JlK^3*R-SW`9!Ln%14WzRuUJw9S}-K@1zCOP zRuq4Sb;3LTkd02%v~?w;zoDKH@-Pu3Tt-6!H2E()YG8d9Neu~uLXx#-b&kK^2o6m3 z{&oDX{FjU`fVDt*30}nFAZ6uOje_OKV zw;;Bac-U~IQtD^E+81~kO0ftN<-cwW%rfMz*IoxpLPUvUa!-Mw*jBVT{RP5Q{drXB z`0T#%nthe_E7HoBUp7_W|DFI{zTeQ>`wIf!UOgY*ltM#3slgo!lp+2Kp^_p+Og=VY zIkn|d-7Zm%egk3d+%z}RK9oILqIs#{hPp^#Db7M-;>gO*aYIdgk9kJihM+s}`1ayy zF(r0n#I`OpG6E)E82+&4zt=ZjYrW3>op>r^Ml4p9_;>E$=Ji-~2LDNMyPDNHG94gF#5v-{vIOA+C# zvspeemC5{%eq62jIcy5F?ijo6r#^tJj-Jkc_0(t1 z+p{pI#)MkwGpnjeQ4g7m zH_tryl;Spv74_k9ql~Hn(uEWv+H5GkVU$lo(pjfyTfpqtI!RoP|5nPGxI?|~aXe0? zrehfyOF~AuG`46clEZ`&MU*9DH)9zQW*lo0XT~=6Ju1Y6gfd7|XRK4U9I`dYRx}u7 z8pD~KpO$;>y-!c~KF|Fde$VrJ-rx7@_5R=tPGVd%SVqPu??_x3@uM-fSu`99ETN8J5a(&87C%Uz8(Z{0&Uv*ceE+GV!E0uWUneolVl!K^jj)iO zu-$eOm6u6SFsGrM{`{9V=a-c>Az!?p%q8UYsh4?zkD)I3h6v93NqEbQw%yYqxcsF4 zpd=ritA`06gTE!W+Z)0cBaXDNCZfvit#uzFS-xAhHT=Hb-GI|v87|hRT3jre;x*?F z58iO-ptEyH?Pd*?`DNqo9oWQJ2zJecb?e0Y!`Dy=ut}1~ToyG6UO%h8Oi z)xBcxK)5cuaoaqFE7z5A13muM2}RSp5ptEu9y#LQn5u9L@@`jAm2^ zN$#vSK|9Rs`(WC@AeE{(QYJ#waOpO6GIZ{EI&f8lWk;-HviOkJ&leVRO761DN7<8n zgCEgGwe?cnxnf|8ZQ|UP(#mp~k&&6fy75q4h?HDuT3<(qIa}n!;C2U1{y9BCYu(7+ zo5WR}3Yuzw%!?x}XqRTkOxLjN0hqYjK}~^fuh1#5Mzmb`B~Vj%qZ03I-hf-qL{YnJ zHBJZTt?}6=1Kh7@5%%4^zo^P}el@@z$eB-}?>1wsbbb5_3l#+0j@z2O82wzsanc{OO8yCX@sF+RER$c}haj`M22 z`!{46Y%jVco~;p;+(LY+mZl*?7mn_>DBHxGd?MBj65&z_6~Sh8i1kh~Sqa?Bk^62; z>oSN0TPG1>79tU>_;JpNiO45eNr^P&Dtbm_wJpRepH`}rc`?Yj+ zr6Ca?M-QJnp8|l-J0io14zJHXK~XUH0K86xj=+!;QSy(~koGTl)B~&cwmI|;8DWKZ ziPaFmcy|bccjyfZIH3b^+8}A_US6qg`N709-Ro`JW)<@A(O36a5WW32JKb$Eo=Hd+ zoV9Z8k@^sxL(5H@VcA7Pyg|w8ry#T2(Fm`V-k7Vjutv~jP}ZAI$DB6KK35wpNPO{I zo_q_rFUp&t{iL%SB#HMlBWL}pTL;>alxtWK6cAXWvhYl7!IuvJAF?v<-U`;p&i=FE zED&0YrOff~B;D}w<`*ua8~e}S5`l(wt1E3jSXOH{mMy-e-)Ol?|E?Zq*{a>5d!9i= z1;OTPYe)CT*_{8!&$}dq@yquBHLp$Ejn&nhYX&{&C>=w3dTFTaqHU`M@_L+?L?J>5 zGTNk`kHb9J)xK2D9TG4*eQdHUU(JD0OPOYj99sqr5Fz#1&S(!~wBA&w)e!j;@V}xQSH1A$N0g{oaN~ z@#Cs+Jt;Z}Eac)W#&ah|-q_APlJhk1*flLd)hm`MGwDBR2BVqT7%SZ2YmeV~+Gy?> zh7<%3-1hTSnZF;wC#BG#?mRE--K^;JZVZ&pBs9S-jBfu;9`|2OYNx7C7n>Q*$+FK3 z)^F4ZL9;k1=p1a7iY>t)K>k7{PH-ijx>v31LiD_0YgH(@M;|6SCUxT;gI=1CFd9!< z2K@TuAT4iZExlw<%9Boct)$ns%q&4>3^hTd2e)*vlM+LV=p9qL41Czsui*AuGZ!i? z`&Ca7`gPo2sllJ$$j}(D_jYEACD80Pl~Vh`bwkfpW&3gx^Jdm)r?aIu^yq4>?R`|l zbQefqIzfY)esN$`!Rm5l!S$%mDqkA8ljgBEb^Pbnha6i115+dPA?%Qr0Hm+~gjv#U zlnGBwrR7wQ%c(z_u-R7mJZX09?AYaii}Na*`qWYi-Em~-rM|Dm5gZVWhcM!n8(Qp1s#2e4otQdvMaIb%ik+QSl(`UOc^IlIy{uU}m^pY>Ed{15n})e0OD_ zv}WK`)yGSCf29O*Jqg$dI|FDlA~>Q7A0|w|>{`!-ZFv2!`vPE*1I(D`^8eewfC&WS zkslj|OGm+otvnvl{D0)}{x4eqz|oy@o!ash0|})!elUa%irf?o3Q9nMm^feH#OAg$ MhUZRI8aPG$1xgs_kpKVy literal 20104 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

Ss`Nf+mJuGz=z znW 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 KgE>KQIe!C94Wv;3 diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 6bf41d8329..3a109d07f4 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -12,9 +12,10 @@ enum CollabEvents { // Send ROOM_FULL = "room_full", CONNECTED = "connected", + NEW_USER_CONNECTED = "new_user_connected", CODE_CHANGE = "code_change", - LEFT = "left", - DISCONNECTED = "disconnected", + PARTNER_LEFT = "partner_left", + PARTNER_DISCONNECTED = "partner_disconnected", } const EXPIRY_TIME = 3600; @@ -34,13 +35,12 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { socket.join(roomId); socket.data.roomId = roomId; - // in case of disconnect, send the code to the user when rejoin + // in case of disconnect, send the code to the user when he rejoins const code = await redisClient.get(`collaboration:${roomId}`); - if (code) { - io.to(roomId).emit(CollabEvents.CONNECTED, { code }); - } else { - io.to(roomId).emit(CollabEvents.CONNECTED); - } + socket.emit(CollabEvents.CONNECTED, { code: code ? code : "" }); + + // inform the other user that a new user has joined + socket.to(roomId).emit(CollabEvents.NEW_USER_CONNECTED); }); socket.on(CollabEvents.CHANGE, async ({ roomId, code }) => { @@ -51,7 +51,7 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { await redisClient.set(`collaboration:${roomId}`, code, { EX: EXPIRY_TIME, }); - io.to(roomId).emit(CollabEvents.CODE_CHANGE, { code }); + socket.to(roomId).emit(CollabEvents.CODE_CHANGE, { code }); }); socket.on(CollabEvents.LEAVE, ({ roomId }) => { @@ -60,13 +60,13 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { } socket.leave(roomId); - socket.to(roomId).emit(CollabEvents.LEFT); + socket.to(roomId).emit(CollabEvents.PARTNER_LEFT); }); socket.on(CollabEvents.DISCONNECT, () => { const { roomId } = socket.data; if (roomId) { - socket.to(roomId).emit(CollabEvents.DISCONNECTED); + socket.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED); } }); }; From 20336e874eaa0b477694db0377201780a298673b Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 27 Oct 2024 10:42:56 +0800 Subject: [PATCH 014/192] Update readme --- backend/collab-service/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/collab-service/README.md b/backend/collab-service/README.md index 920fdb0141..fd96790327 100644 --- a/backend/collab-service/README.md +++ b/backend/collab-service/README.md @@ -44,9 +44,9 @@ ![image4.png](docs/image4.png) ## Events Available -| Event Name | Description | Parameters | Response Event | -|----------------|-----------------------------------|-------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **join** | Joins a collaboration room. | `roomId` (string): ID of the room. | **room_full:** Notify the user if the room is full (only 2 users allowed).
**connected:** Notify the user if successfully connection.
**new_user_connected:** Notify the other user if a new user joins the room. | -| **change** | Sends updated code to other user. | `roomId` (string): ID of the room.
`code` (string): Updated code content. | **code_change:** Notify the other user with the updated code content. | -| **leave** | Leaves the collaboration room. | `roomId` (string): ID of the room. | **partner_left:** Notify the other user when one leaves the room. | -| **disconnect** | Disconnects from the server. | None | **partner_disconnected:** Notify the other user when one is disconnected. | \ No newline at end of file +| Event Name | Description | Parameters | Response Event | +|----------------|-----------------------------------|-------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **join** | Joins a collaboration room. | `roomId` (string): ID of the room. | **room_full:** Notify the user if the room is full (only 2 users allowed).
**connected:** Notify the user if successfully connected.
**new_user_connected:** Notify the other user if a new user joins the room. | +| **change** | Sends updated code to other user. | `roomId` (string): ID of the room.
`code` (string): Updated code content. | **code_change:** Notify the other user with the updated code content. | +| **leave** | Leaves the collaboration room. | `roomId` (string): ID of the room. | **partner_left:** Notify the other user when one leaves the room. | +| **disconnect** | Disconnects from the server. | None | **partner_disconnected:** Notify the other user when one is disconnected. | \ No newline at end of file From 1c268e27a739a7bb7d5c2488fd9629106950aa3f Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 27 Oct 2024 13:42:44 +0800 Subject: [PATCH 015/192] Update swagger --- .../controllers/codeExecutionControllers.ts | 31 +++++++--- .../src/utils/constants.ts | 13 ++++ backend/code-execution-service/swagger.yml | 62 ++++++++++++++----- .../src/controllers/questionController.ts | 2 + 4 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 backend/code-execution-service/src/utils/constants.ts diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index 68c0b737b8..b2531191f7 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -1,5 +1,13 @@ import { Request, Response } from "express"; import { oneCompilerApi } from "../utils/oneCompilerApi"; +import { + SUPPORTED_LANGUAGES, + ERROR_MISSING_REQUIRED_FIELDS_MESSAGE, + ERROR_UNSUPPORTED_LANGUAGE_MESSAGE, + ERROR_FAILED_TO_EXECUTE_MESSAGE, + ERROR_NOT_SAME_LENGTH_MESSAGE, + SUCCESS_MESSAGE, +} from "../utils/constants"; interface CompilerResult { status: string; @@ -20,21 +28,27 @@ export const executeCode = async (req: Request, res: Response) => { if (!language || !code || !stdinList || !expectedStdoutList) { res.status(400).json({ - error: - "Missing required fields: language, code, stdinList, or stdoutList", + message: ERROR_MISSING_REQUIRED_FIELDS_MESSAGE, }); } + if (!SUPPORTED_LANGUAGES.includes(language)) { + res.status(400).json({ + message: ERROR_UNSUPPORTED_LANGUAGE_MESSAGE, + }); + return; + } + if (stdinList.length !== expectedStdoutList.length) { res.status(400).json({ - error: "The length of stdinList and stdoutList must be the same.", + message: ERROR_NOT_SAME_LENGTH_MESSAGE, }); } try { const response = await oneCompilerApi(language, stdinList, code); - const results = (response.data as CompilerResult[]).map((result, index) => { + const data = (response.data as CompilerResult[]).map((result, index) => { const { status, exception, @@ -61,11 +75,10 @@ export const executeCode = async (req: Request, res: Response) => { }); res.status(200).json({ - message: "Code executed successfully", - results, + message: SUCCESS_MESSAGE, + data, }); - } catch (error) { - console.error("Error executing code:", error); - res.status(500).json({ error: "Failed to execute code" }); + } catch { + res.status(500).json({ message: ERROR_FAILED_TO_EXECUTE_MESSAGE }); } }; diff --git a/backend/code-execution-service/src/utils/constants.ts b/backend/code-execution-service/src/utils/constants.ts new file mode 100644 index 0000000000..1e5fb3aed2 --- /dev/null +++ b/backend/code-execution-service/src/utils/constants.ts @@ -0,0 +1,13 @@ +export const SUPPORTED_LANGUAGES = ["python", "java", "c"]; + +export const ERROR_MISSING_REQUIRED_FIELDS_MESSAGE = + "Missing required fields: language, code, stdinList, or stdoutList"; + +export const ERROR_UNSUPPORTED_LANGUAGE_MESSAGE = "Unsupported language."; + +export const ERROR_NOT_SAME_LENGTH_MESSAGE = + "The length of stdinList and stdoutList must be the same."; + +export const ERROR_FAILED_TO_EXECUTE_MESSAGE = "Failed to execute code"; + +export const SUCCESS_MESSAGE = "Code executed successfully"; diff --git a/backend/code-execution-service/swagger.yml b/backend/code-execution-service/swagger.yml index 4e3ab0f085..a2518f4ead 100644 --- a/backend/code-execution-service/swagger.yml +++ b/backend/code-execution-service/swagger.yml @@ -4,6 +4,45 @@ info: title: Code Execution Service version: 1.0.0 +definitions: + CodeExecutionOutput: + properties: + stdin: + type: string + required: true + expectedStdout: + type: string + required: true + actualStdout: + type: string + isMatch: + type: boolean + status: + type: string + exception: + type: string + nullable: true + stderr: + type: string + nullable: true + executionTime: + type: integer + +components: + schemas: + CodeExecutionResponse: + properties: + message: + type: string + data: + type: array + items: + $ref: "#/definitions/CodeExecutionOutput" + ErrorResponse: + properties: + message: + type: string + paths: /: get: @@ -43,7 +82,7 @@ paths: code: type: string description: The source code to execute. - example: "name = input()\nage = input()\nprint('Hello ' + name + '. You are ' + age + '?')\n\n" + example: "name = input()\nage = input()\nprint('Hello ' + name + '. You are ' + age + ' years old this year?')\n\n" stdinList: type: array description: List of standard input values to pass to the code. @@ -56,31 +95,26 @@ paths: items: type: string example: - ["Hello Alice. You are 21?\n", "Hello Peter. You are 22?\n"] + [ + "Hello Alice. You are 21 years old this year?\n", + "Hello Peter. You are 22 years old this year?\n", + ] responses: 200: description: Execution Result content: application/json: schema: - type: object + $ref: "#/components/schemas/CodeExecutionResponse" 400: - description: Bad Request - Missing Required Fields + description: Bad Request content: application/json: schema: - type: object - properties: - error: - type: string - example: "Missing required fields: language, code, stdinList, or stdoutList" + $ref: "#/components/schemas/ErrorResponse" 500: description: Internal Server Error content: application/json: schema: - type: object - properties: - error: - type: string - example: "Failed to execute code" + $ref: "#/components/schemas/ErrorResponse" diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 39b7b8ce49..0b095cb044 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -279,6 +279,8 @@ export const readQuestionIndiv = async ( questionId: id, }); + console.log(questionTemplate, "!!!!") + res.status(200).json({ message: QN_RETRIEVED_MESSAGE, question: formatQuestionIndivResponse( From 83b277675f972223ec6405ee42cefc734c48f9fb Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:04:32 +0800 Subject: [PATCH 016/192] Update swagger for question service --- .../src/controllers/questionController.ts | 8 +- backend/question-service/swagger.yml | 74 +++++++++++++++++-- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 0b095cb044..eb33b937ae 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -279,8 +279,6 @@ export const readQuestionIndiv = async ( questionId: id, }); - console.log(questionTemplate, "!!!!") - res.status(200).json({ message: QN_RETRIEVED_MESSAGE, question: formatQuestionIndivResponse( @@ -336,8 +334,8 @@ const formatQuestionIndivResponse = ( description: question.description, complexity: question.complexity, categories: question.category, - pythonTemplate: questionTemplate.pythonTemplate, - javaTemplate: questionTemplate.javaTemplate, - cTemplate: questionTemplate.cTemplate, + pythonTemplate: questionTemplate ? questionTemplate.pythonTemplate : "", + javaTemplate: questionTemplate ? questionTemplate.javaTemplate : "", + cTemplate: questionTemplate ? questionTemplate.cTemplate : "", }; }; diff --git a/backend/question-service/swagger.yml b/backend/question-service/swagger.yml index 249b6ea0b4..9dacdefec3 100644 --- a/backend/question-service/swagger.yml +++ b/backend/question-service/swagger.yml @@ -29,6 +29,32 @@ components: type: string description: Categories + QuestionIndiv: + properties: + title: + type: string + description: Title + description: + type: string + description: Description + complexity: + type: string + description: Complexity - Easy, Medium, Hard + category: + type: array + items: + type: string + description: Categories + pythonTemplate: + type: string + description: Code template in Python + javaTemplate: + type: string + description: Code template in Java + cTemplate: + type: string + description: Code template in C + definitions: Question: type: object @@ -59,6 +85,44 @@ definitions: __v: type: string description: Document version + QuestionIndiv: + type: object + properties: + _id: + type: string + description: Question id + title: + type: string + description: Title + description: + type: string + description: Description + complexity: + type: string + description: Complexity - Easy, Medium, Hard + category: + type: array + items: + type: string + description: Categories + pythonTemplate: + type: string + description: Code template in Python + javaTemplate: + type: string + description: Code template in Java + cTemplate: + type: string + description: Code template in C + createdAt: + type: string + description: Date of creation + updatedAt: + type: string + description: Latest update + __v: + type: string + description: Document version Error: type: object properties: @@ -106,7 +170,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Question" + $ref: "#/components/schemas/QuestionIndiv" responses: 201: description: Created @@ -119,7 +183,7 @@ paths: type: string description: Message question: - $ref: "#/definitions/Question" + $ref: "#/definitions/QuestionIndiv" 400: description: Bad Request content: @@ -224,7 +288,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Question" + $ref: "#/components/schemas/QuestionIndiv" responses: 200: description: Successful Response @@ -237,7 +301,7 @@ paths: type: string description: Message question: - $ref: "#/definitions/Question" + $ref: "#/definitions/QuestionIndiv" 404: description: Question Not Found content: @@ -309,7 +373,7 @@ paths: type: string description: Message question: - $ref: "#/definitions/Question" + $ref: "#/definitions/QuestionIndiv" 404: description: Question Not Found content: From 7bec9b48fd2cd6065cc78f91a37a135101391b3f Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 27 Oct 2024 14:37:47 +0800 Subject: [PATCH 017/192] Add type for QuestionDetail --- frontend/src/reducers/questionReducer.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts index 3cf4f9d7d4..3fa5535f8d 100644 --- a/frontend/src/reducers/questionReducer.ts +++ b/frontend/src/reducers/questionReducer.ts @@ -8,13 +8,21 @@ type QuestionDetail = { description: string; complexity: string; categories: Array; - pythonTemplate?: string; - javaTemplate?: string; - cTemplate?: string; + pythonTemplate: string; + javaTemplate: string; + cTemplate: string; +}; + +type QuestionListDetail = { + id: string; + title: string; + description: string; + complexity: string; + categories: Array; }; type QuestionList = { - questions: Array; + questions: Array; questionCount: number; }; @@ -38,7 +46,7 @@ type QuestionActions = { type QuestionsState = { questionCategories: Array; - questions: Array; + questions: Array; questionCount: number; selectedQuestion: QuestionDetail | null; questionCategoriesError: string | null; From c11b5812cbc34a55faec90913bfb6a4d12514c68 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 27 Oct 2024 19:10:43 +0800 Subject: [PATCH 018/192] Add collab service into ci --- .github/workflows/ci.yml | 3 +- backend/collab-service/jest.config.ts | 199 +++++++++++ backend/collab-service/package-lock.json | 316 ++++++++++++++++++ backend/collab-service/package.json | 3 + .../tests/webSocketHandler.spec.ts | 5 + docker-compose-test.yml | 22 +- 6 files changed, 541 insertions(+), 7 deletions(-) create mode 100644 backend/collab-service/jest.config.ts create mode 100644 backend/collab-service/tests/webSocketHandler.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7dceb180d..a417284f18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - service: [question-service, user-service, matching-service] + service: + [question-service, user-service, matching-service, collab-service] steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/backend/collab-service/jest.config.ts b/backend/collab-service/jest.config.ts new file mode 100644 index 0000000000..151d29ec19 --- /dev/null +++ b/backend/collab-service/jest.config.ts @@ -0,0 +1,199 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +import type { Config } from "jest"; + +const config: Config = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/04/ng4c26hj1ksdsy_7x_21kvx80000gn/T/jest_dx", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + preset: "ts-jest", + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: "jest-environment-node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; + +export default config; diff --git a/backend/collab-service/package-lock.json b/backend/collab-service/package-lock.json index 385895992d..2fc49f9eb3 100644 --- a/backend/collab-service/package-lock.json +++ b/backend/collab-service/package-lock.json @@ -30,6 +30,8 @@ "eslint": "^9.13.0", "globals": "^15.11.0", "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", "tsx": "^4.19.1", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0" @@ -702,6 +704,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -1866,6 +1892,34 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2482,6 +2536,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2555,6 +2622,13 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2571,6 +2645,13 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2800,6 +2881,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -3094,6 +3188,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -3224,6 +3325,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -3252,6 +3363,22 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.45", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.45.tgz", @@ -3883,6 +4010,39 @@ "node": ">=16.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4591,6 +4751,25 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -5326,6 +5505,13 @@ "node": ">=8" } }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5372,6 +5558,13 @@ "node": ">=10" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -6613,6 +6806,112 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsx": { "version": "4.19.1", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz", @@ -6785,6 +7084,13 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -6962,6 +7268,16 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index e2b7d4ad94..9a3bf330e0 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -7,6 +7,7 @@ "start": "tsx server.ts", "dev": "tsx watch server.ts", "test": "cross-env NODE_ENV=test && jest", + "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." }, "author": "", @@ -34,6 +35,8 @@ "eslint": "^9.13.0", "globals": "^15.11.0", "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", "tsx": "^4.19.1", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0" diff --git a/backend/collab-service/tests/webSocketHandler.spec.ts b/backend/collab-service/tests/webSocketHandler.spec.ts new file mode 100644 index 0000000000..1b286ac176 --- /dev/null +++ b/backend/collab-service/tests/webSocketHandler.spec.ts @@ -0,0 +1,5 @@ +describe("Test web socket", () => { + it("Test", () => { + expect(true); + }); +}); diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 6e6b60e26e..f9b7006115 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -4,7 +4,6 @@ services: test-user-service: image: peerprep/user-service build: ./backend/user-service - # env_file: ./backend/user-service/.env environment: - NODE_ENV=test - SERVICE_PORT=3001 @@ -34,7 +33,6 @@ services: test-question-service: image: peerprep/question-service build: ./backend/question-service - # env_file: ./backend/question-service/.env environment: - NODE_ENV=test - SERVICE_PORT=3000 @@ -56,10 +54,9 @@ services: test-matching-service: image: peerprep/matching-service build: ./backend/matching-service - # env_file: ./backend/matching-service/.env environment: - NODE_ENV=test - - PORT=3002 + - SERVICE_PORT=3002 - RABBITMQ_DEFAULT_USER=admin - RABBITMQ_DEFAULT_PASS=password - RABBITMQ_ADDR=amqp://admin:password@rabbitmq:5672 @@ -71,6 +68,21 @@ services: restart: on-failure command: ["npm", "test"] + test-collab-service: + image: peerprep/collab-service + build: ./backend/collab-service + environment: + - NODE_ENV=test + - SERVICE_PORT=3003 + - REDIS_URI_TEST=redis://test-redis:6379 + networks: + - peerprep-network + volumes: + - ./backend/collab-service:/collab-service + - /collab-service/node_modules + restart: on-failure + command: ["npm", "test"] + test-frontend: image: peerprep/frontend build: ./frontend @@ -87,8 +99,6 @@ services: restart: always networks: - peerprep-network - # env_file: - # - ./backend/.env environment: - MONGO_INITDB_ROOT_USERNAME=mongo - MONGO_INITDB_ROOT_PASSWORD=mongo From ee76f26c560658230c92ef5743b49909f21c9c62 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 27 Oct 2024 19:38:11 +0800 Subject: [PATCH 019/192] Fix linting --- backend/question-service/src/controllers/questionController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index a6e0694aff..dc6fdb8030 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -240,6 +240,7 @@ export const readQuestionIndiv = async ( }; export const readRandomQuestion = async ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any req: Request, res: Response, ): Promise => { From 21c25dd2c1373f40d4b4ac6f87f6ec20f6179ea2 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Mon, 28 Oct 2024 00:30:27 +0800 Subject: [PATCH 020/192] Add navbar collab session controls --- .../CollabSessionControls/index.tsx | 68 +++++++++++++++++++ frontend/src/components/Navbar/index.tsx | 5 +- frontend/src/components/Stopwatch/index.tsx | 29 ++++++++ frontend/src/utils/sessionTime.ts | 6 ++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/CollabSessionControls/index.tsx create mode 100644 frontend/src/components/Stopwatch/index.tsx create mode 100644 frontend/src/utils/sessionTime.ts diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx new file mode 100644 index 0000000000..a5ff015224 --- /dev/null +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -0,0 +1,68 @@ +import { Button, Stack } from "@mui/material"; +import Stopwatch from "../Stopwatch"; +import { useMatch } from "../../contexts/MatchContext"; +import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; +import { useEffect, useState } from "react"; +import { + extractHoursFromTime, + extractMinutesFromTime, + extractSecondsFromTime, +} from "../../utils/sessionTime"; + +const CollabSessionControls: React.FC = () => { + const [time, setTime] = useState(0); + + useEffect(() => { + let intervalId = setInterval( + () => setTime((prevTime) => prevTime + 1), + 1000 + ); + + return () => clearInterval(intervalId); + }, [time]); + + const match = useMatch(); + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + const { stopMatch } = match; + + return ( + + + + + + ); +}; + +export default CollabSessionControls; diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index bdb98a8880..d3e74e8e78 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -23,6 +23,7 @@ import { } from "../../utils/constants"; import { isMatchingPage, isCollabPage } from "../../utils/url"; import { useMatch } from "../../contexts/MatchContext"; +import CollabSessionControls from "../CollabSessionControls"; type NavbarItem = { label: string; link: string; needsLogin: boolean }; @@ -83,7 +84,9 @@ const Navbar: React.FC = (props) => { PeerPrep {isCollabPage(path) ? ( - <> + <> + + ) : !isMatchingPage(path) ? ( {navbarItems diff --git a/frontend/src/components/Stopwatch/index.tsx b/frontend/src/components/Stopwatch/index.tsx new file mode 100644 index 0000000000..7a958464be --- /dev/null +++ b/frontend/src/components/Stopwatch/index.tsx @@ -0,0 +1,29 @@ +import { + extractHoursFromTime, + extractMinutesFromTime, + extractSecondsFromTime, +} from "../../utils/sessionTime"; + +interface StopwatchProps { + time: number; +} + +const Stopwatch: React.FC = (props) => { + const { time } = props; + + const hours = extractHoursFromTime(time); + const minutes = extractMinutesFromTime(time); + const seconds = extractSecondsFromTime(time); + + return ( +

+

+ {hours.toString().padStart(2, "0")}: + {minutes.toString().padStart(2, "0")}: + {seconds.toString().padStart(2, "0")} +

+
+ ); +}; + +export default Stopwatch; diff --git a/frontend/src/utils/sessionTime.ts b/frontend/src/utils/sessionTime.ts new file mode 100644 index 0000000000..a5802acb62 --- /dev/null +++ b/frontend/src/utils/sessionTime.ts @@ -0,0 +1,6 @@ +export const extractHoursFromTime = (time: number) => Math.floor(time / 3600); + +export const extractMinutesFromTime = (time: number) => + Math.floor((time % 3600) / 60); // after extracting hours + +export const extractSecondsFromTime = (time: number) => time % 60; // after extracting hours and minutes From 362f0e57c3afc54a531a7fc9c724d05158c818ef Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Mon, 28 Oct 2024 00:43:10 +0800 Subject: [PATCH 021/192] Add to ci and test --- .github/workflows/ci.yml | 9 +- backend/code-execution-service/jest.config.ts | 199 +++++ .../code-execution-service/package-lock.json | 711 +++++++++++------- backend/code-execution-service/package.json | 5 + .../controllers/codeExecutionControllers.ts | 2 + .../tests/codeExecutionRoutes.spec.ts | 65 ++ docker-compose-test.yml | 14 + 7 files changed, 725 insertions(+), 280 deletions(-) create mode 100644 backend/code-execution-service/jest.config.ts create mode 100644 backend/code-execution-service/tests/codeExecutionRoutes.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a417284f18..27ccd7ab04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,13 @@ jobs: strategy: matrix: service: - [question-service, user-service, matching-service, collab-service] + [ + question-service, + user-service, + matching-service, + collab-service, + code-execution-service, + ] steps: - name: Checkout code uses: actions/checkout@v4 @@ -55,4 +61,5 @@ jobs: FIREBASE_CLIENT_EMAIL: ${{ secrets.FIREBASE_CLIENT_EMAIL }} FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }} JWT_SECRET: ${{ secrets.JWT_SECRET }} + ONE_COMPILER_KEY: ${{ secrets.ONE_COMPILER_KEY }} run: docker compose -f docker-compose-test.yml run --rm test-${{ matrix.service }} diff --git a/backend/code-execution-service/jest.config.ts b/backend/code-execution-service/jest.config.ts new file mode 100644 index 0000000000..151d29ec19 --- /dev/null +++ b/backend/code-execution-service/jest.config.ts @@ -0,0 +1,199 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +import type { Config } from "jest"; + +const config: Config = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/04/ng4c26hj1ksdsy_7x_21kvx80000gn/T/jest_dx", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + preset: "ts-jest", + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: "jest-environment-node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; + +export default config; diff --git a/backend/code-execution-service/package-lock.json b/backend/code-execution-service/package-lock.json index 11924dc89a..09e3153a46 100644 --- a/backend/code-execution-service/package-lock.json +++ b/backend/code-execution-service/package-lock.json @@ -15,6 +15,8 @@ "dotenv": "^16.4.5", "express": "^4.21.1", "swagger-ui-express": "^5.0.1", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", "yaml": "^2.6.0" }, "devDependencies": { @@ -23,11 +25,13 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.9", + "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "cross-env": "^7.0.3", "eslint": "^9.13.0", "globals": "^15.11.0", "jest": "^29.7.0", + "supertest": "^7.0.0", "tsx": "^4.19.1", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0" @@ -37,7 +41,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -51,7 +54,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.9.tgz", "integrity": "sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/highlight": "^7.25.9", @@ -65,7 +67,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.9.tgz", "integrity": "sha512-yD+hEuJ/+wAJ4Ox2/rpNv5HIuPG82x3ZlQvYVn8iYCprdxzE7P1udpGF1jyjQVBU4dgznN+k2h103vxZ7NdPyw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -75,7 +76,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.9.tgz", "integrity": "sha512-WYvQviPw+Qyib0v92AwNIrdLISTp7RfDkM7bPqBvpbnhY4wq8HvHBZREVdYDXk98C8BkOIVnHAY3yvj7AVISxQ==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -106,7 +106,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -124,14 +123,12 @@ "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/@babel/generator": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.9.tgz", "integrity": "sha512-omlUGkr5EaoIJrhLf9CJ0TvjBRpd9+AXRG//0GEQ9THSo8wPiTlbpy1/Ow8ZTrbXpjd9FHXfbFQx32I04ht0FA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.25.9", @@ -147,7 +144,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.25.9", @@ -164,7 +160,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -178,7 +173,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.9.tgz", "integrity": "sha512-TvLZY/F3+GvdRYFZFyxMvnsKi+4oJdgZzU3BoGN9Uc2d9C6zfNwJcKKhjqLAhK8i46mv93jsO74fDh3ih6rpHA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -197,7 +191,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -207,7 +200,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -221,7 +213,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -231,7 +222,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -241,7 +231,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -251,7 +240,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.9.tgz", "integrity": "sha512-oKWp3+usOJSzDZOucZUAMayhPz/xVjzymyDzUN8dk0Wd3RWMlGLXi07UCQ/CgQVb8LvXx3XBajJH4XGgkt7H7g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -265,7 +253,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -281,7 +268,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -294,7 +280,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -309,7 +294,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -319,14 +303,12 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, "license": "MIT" }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.0" @@ -336,7 +318,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -346,7 +327,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -359,7 +339,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.25.9" @@ -375,7 +354,6 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -388,7 +366,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -401,7 +378,6 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" @@ -414,7 +390,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -430,7 +405,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.9.tgz", "integrity": "sha512-u3EN9ub8LyYvgTnrgp8gboElouayiwPdnM7x5tcnW3iSt09/lQYPwMNK40I9IUxo7QOZhAsPHCmmuO7EPdruqg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -446,7 +420,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -459,7 +432,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -472,7 +444,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -488,7 +459,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -501,7 +471,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -514,7 +483,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" @@ -527,7 +495,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -540,7 +507,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -553,7 +519,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" @@ -566,7 +531,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -582,7 +546,6 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" @@ -598,7 +561,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -614,7 +576,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.25.9", @@ -629,7 +590,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.25.9", @@ -648,7 +608,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -666,7 +625,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -676,14 +634,12 @@ "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/@babel/types": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -697,9 +653,28 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, "license": "MIT" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -1368,7 +1343,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.3.1", @@ -1385,7 +1359,6 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1395,7 +1368,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -1413,7 +1385,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -1461,7 +1432,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/fake-timers": "^29.7.0", @@ -1477,7 +1447,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, "license": "MIT", "dependencies": { "expect": "^29.7.0", @@ -1491,7 +1460,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" @@ -1504,7 +1472,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -1522,7 +1489,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -1538,7 +1504,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -1582,7 +1547,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" @@ -1595,7 +1559,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", @@ -1610,7 +1573,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -1626,7 +1588,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -1642,7 +1603,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -1669,7 +1629,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -1687,7 +1646,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -1702,7 +1660,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1712,7 +1669,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1722,14 +1678,12 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1778,14 +1732,12 @@ "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true, "license": "MIT" }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" @@ -1795,17 +1747,35 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", @@ -1819,7 +1789,6 @@ "version": "7.6.8", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -1829,7 +1798,6 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -1840,7 +1808,6 @@ "version": "7.20.6", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" @@ -1867,6 +1834,12 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", @@ -1914,7 +1887,6 @@ "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1931,14 +1903,12 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" @@ -1948,7 +1918,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" @@ -1972,6 +1941,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1983,7 +1958,6 @@ "version": "22.7.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -2030,9 +2004,30 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/swagger-ui-express": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", @@ -2048,7 +2043,6 @@ "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "dev": true, "license": "MIT", "dependencies": { "@types/yargs-parser": "*" @@ -2058,7 +2052,6 @@ "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -2395,7 +2388,6 @@ "version": "8.13.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2414,6 +2406,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2435,7 +2438,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.21.3" @@ -2451,7 +2453,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2461,7 +2462,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2477,7 +2477,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -2487,11 +2486,15 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" @@ -2503,6 +2506,17 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2523,7 +2537,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, "license": "MIT", "dependencies": { "@jest/transform": "^29.7.0", @@ -2545,7 +2558,6 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -2562,7 +2574,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", @@ -2579,7 +2590,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", @@ -2595,7 +2605,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", @@ -2622,7 +2631,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", @@ -2639,7 +2647,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/body-parser": { @@ -2670,7 +2677,6 @@ "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", "dependencies": { "balanced-match": "^1.0.0", @@ -2681,7 +2687,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -2694,7 +2699,6 @@ "version": "4.24.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2723,11 +2727,21 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" @@ -2737,7 +2751,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/bytes": { @@ -2772,7 +2785,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2782,7 +2794,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2792,7 +2803,6 @@ "version": "1.0.30001669", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2813,7 +2823,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -2830,7 +2839,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2840,7 +2848,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, "funding": [ { "type": "github", @@ -2856,14 +2863,12 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", - "dev": true, "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -2878,7 +2883,6 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, "license": "MIT", "engines": { "iojs": ">= 1.0.0", @@ -2889,14 +2893,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true, "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2909,7 +2911,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -2924,11 +2925,19 @@ "node": ">= 0.8" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/content-disposition": { @@ -2956,7 +2965,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -2974,6 +2982,12 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2991,7 +3005,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -3009,6 +3022,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -3032,7 +3050,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3056,7 +3073,6 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -3078,7 +3094,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3133,17 +3148,33 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3166,18 +3197,30 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.45", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.45.tgz", "integrity": "sha512-vOzZS6uZwhhbkZbcRyiy99Wg+pYFV5hk+5YaECvx0+Z31NR3Tt5zS6dze2OepT6PCTzVzT0dIJItti+uAW5zmw==", - "dev": true, "license": "ISC" }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3190,7 +3233,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -3206,7 +3248,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -3277,7 +3318,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3293,7 +3333,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3499,7 +3538,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -3568,7 +3606,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -3592,7 +3629,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -3601,7 +3637,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", @@ -3697,7 +3732,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -3707,6 +3741,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -3721,7 +3761,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "bser": "2.1.1" @@ -3740,11 +3779,37 @@ "node": ">=16.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3775,7 +3840,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -3840,6 +3904,20 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", + "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "dev": true, + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^2.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3862,14 +3940,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -3893,7 +3969,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3903,7 +3978,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -3932,7 +4006,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.0.0" @@ -3942,7 +4015,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -3969,7 +4041,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -4028,7 +4099,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -4042,7 +4112,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4096,11 +4165,19 @@ "node": ">= 0.4" } }, + "node_modules/hexoid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", + "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, "license": "MIT" }, "node_modules/http-errors": { @@ -4123,7 +4200,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -4182,7 +4258,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", @@ -4202,7 +4277,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -4213,7 +4287,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -4239,14 +4312,12 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -4272,7 +4343,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4282,7 +4352,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4305,7 +4374,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4315,7 +4383,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4328,14 +4395,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -4345,7 +4410,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", @@ -4362,7 +4426,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4375,7 +4438,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -4390,7 +4452,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", @@ -4405,7 +4466,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4423,14 +4483,12 @@ "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/istanbul-reports": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -4440,11 +4498,27 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -4471,7 +4545,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, "license": "MIT", "dependencies": { "execa": "^5.0.0", @@ -4486,7 +4559,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -4518,7 +4590,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -4552,7 +4623,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -4598,7 +4668,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -4614,7 +4683,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" @@ -4627,7 +4695,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -4644,7 +4711,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -4662,7 +4728,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4672,7 +4737,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -4698,7 +4762,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", @@ -4712,7 +4775,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -4728,7 +4790,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", @@ -4749,7 +4810,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -4764,7 +4824,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4782,7 +4841,6 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4792,7 +4850,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -4813,7 +4870,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", @@ -4827,7 +4883,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -4860,7 +4915,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -4894,7 +4948,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -4926,7 +4979,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4939,7 +4991,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -4957,7 +5008,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -4975,7 +5025,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -4988,7 +5037,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -5008,7 +5056,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -5024,7 +5071,6 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -5040,14 +5086,12 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -5061,7 +5105,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -5081,7 +5124,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -5102,7 +5144,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -5125,7 +5166,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5135,7 +5175,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5159,14 +5198,12 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -5175,6 +5212,11 @@ "node": ">=8" } }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5186,7 +5228,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -5196,7 +5237,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -5212,7 +5252,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5221,11 +5260,15 @@ "node": ">=10" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "tmpl": "1.0.5" @@ -5253,7 +5296,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, "license": "MIT" }, "node_modules/merge2": { @@ -5279,7 +5321,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5326,7 +5367,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5336,7 +5376,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -5355,7 +5394,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, "license": "MIT" }, "node_modules/negotiator": { @@ -5371,21 +5409,18 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true, "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5395,7 +5430,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -5441,7 +5475,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5451,7 +5484,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -5485,7 +5517,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -5501,7 +5532,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -5514,7 +5544,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -5530,7 +5559,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5553,7 +5581,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -5581,7 +5608,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5591,7 +5617,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5601,7 +5626,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5611,7 +5635,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-to-regexp": { @@ -5624,14 +5647,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5644,7 +5665,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5654,7 +5674,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -5677,7 +5696,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", @@ -5692,7 +5710,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5705,7 +5722,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, "license": "MIT", "dependencies": { "kleur": "^3.0.3", @@ -5748,7 +5764,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, "funding": [ { "type": "individual", @@ -5825,14 +5840,12 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, "license": "MIT" }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5842,7 +5855,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -5860,7 +5872,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" @@ -5873,7 +5884,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5893,7 +5903,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5964,7 +5973,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6051,7 +6059,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6064,7 +6071,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6092,21 +6098,18 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, "license": "ISC" }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, "license": "MIT" }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6116,7 +6119,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6126,7 +6128,6 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -6137,14 +6138,12 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" @@ -6166,7 +6165,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, "license": "MIT", "dependencies": { "char-regex": "^1.0.2", @@ -6180,7 +6178,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6195,7 +6192,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6208,7 +6204,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6218,7 +6213,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6228,7 +6222,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6237,11 +6230,78 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "dev": true, + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -6254,7 +6314,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6288,7 +6347,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", @@ -6310,14 +6368,12 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -6348,6 +6404,106 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsx": { "version": "4.19.1", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz", @@ -6385,7 +6541,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -6395,7 +6550,6 @@ "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -6421,7 +6575,6 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6459,7 +6612,6 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -6475,7 +6627,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6521,11 +6672,15 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -6549,7 +6704,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "makeerror": "1.0.12" @@ -6559,7 +6713,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -6585,7 +6738,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -6603,14 +6755,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -6624,7 +6774,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -6634,7 +6783,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -6653,7 +6801,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -6672,17 +6819,23 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/backend/code-execution-service/package.json b/backend/code-execution-service/package.json index d4dbd84400..9062357cf3 100644 --- a/backend/code-execution-service/package.json +++ b/backend/code-execution-service/package.json @@ -6,6 +6,7 @@ "start": "tsx server.ts", "dev": "tsx watch server.ts", "test": "cross-env NODE_ENV=test && jest", + "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." }, "author": "", @@ -18,6 +19,8 @@ "dotenv": "^16.4.5", "express": "^4.21.1", "swagger-ui-express": "^5.0.1", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", "yaml": "^2.6.0" }, "devDependencies": { @@ -26,11 +29,13 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.9", + "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "cross-env": "^7.0.3", "eslint": "^9.13.0", "globals": "^15.11.0", "jest": "^29.7.0", + "supertest": "^7.0.0", "tsx": "^4.19.1", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0" diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index b2531191f7..6df09b4861 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -30,6 +30,7 @@ export const executeCode = async (req: Request, res: Response) => { res.status(400).json({ message: ERROR_MISSING_REQUIRED_FIELDS_MESSAGE, }); + return; } if (!SUPPORTED_LANGUAGES.includes(language)) { @@ -43,6 +44,7 @@ export const executeCode = async (req: Request, res: Response) => { res.status(400).json({ message: ERROR_NOT_SAME_LENGTH_MESSAGE, }); + return; } try { diff --git a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts new file mode 100644 index 0000000000..202c712927 --- /dev/null +++ b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts @@ -0,0 +1,65 @@ +import supertest from "supertest"; +import app from "../app"; +import { + ERROR_MISSING_REQUIRED_FIELDS_MESSAGE, + ERROR_UNSUPPORTED_LANGUAGE_MESSAGE, + ERROR_NOT_SAME_LENGTH_MESSAGE, + SUCCESS_MESSAGE, +} from "../src/utils/constants"; + +const request = supertest(app); + +const BASE_URL = "/api"; + +describe("Code execution routes", () => { + describe("GET /", () => { + it("should return 200 OK", (done) => { + request.get("/").expect(200, done); + }); + }); + + describe("POST /api/run", () => { + it("should return 400 if required fields are missing", async () => { + const response = await request + .post(`${BASE_URL}/run`) + .send({ language: "python" }); + expect(response.status).toBe(400); + expect(response.body.message).toBe(ERROR_MISSING_REQUIRED_FIELDS_MESSAGE); + }); + + it("should return 400 if the language is unsupported", async () => { + const response = await request.post(`${BASE_URL}/run`).send({ + language: "testing1234", + code: "print('Hello, world!')", + stdinList: ["input"], + stdoutList: ["Hello, world!"], + }); + expect(response.status).toBe(400); + expect(response.body.message).toBe(ERROR_UNSUPPORTED_LANGUAGE_MESSAGE); + }); + + it("should return 400 if stdinList and stdoutList lengths do not match", async () => { + const response = await request.post(`${BASE_URL}/run`).send({ + language: "python", + code: "print('Hello, world!')", + stdinList: ["input1"], + stdoutList: ["output1", "output2"], + }); + expect(response.status).toBe(400); + expect(response.body.message).toBe(ERROR_NOT_SAME_LENGTH_MESSAGE); + }); + + it("should return 200 and execution result when code executes successfully", async () => { + const response = await request.post(`${BASE_URL}/run`).send({ + language: "python", + code: "print(input())", + stdinList: ["Hello, world!"], + stdoutList: ["Hello, world!"], + }); + expect(response.status).toBe(200); + expect(response.body.message).toBe(SUCCESS_MESSAGE); + expect(response.body.data).toBeInstanceOf(Array); + expect(response.body.data[0]).toHaveProperty("isMatch", true); + }); + }); +}); diff --git a/docker-compose-test.yml b/docker-compose-test.yml index f9b7006115..f3cabee5d6 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -83,6 +83,20 @@ services: restart: on-failure command: ["npm", "test"] + test-code-execution-service: + image: peerprep/code-execution-service + build: ./backend/code-execution-service + environment: + - NODE_ENV=test + - SERVICE_PORT=3004 + networks: + - peerprep-network + volumes: + - ./backend/code-execution-service:/code-execution-service + - /code-execution-service/node_modules + restart: on-failure + command: ["npm", "test"] + test-frontend: image: peerprep/frontend build: ./frontend From 1d818dad3d780be2632c1b89b0f1f0e19959ed12 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Mon, 28 Oct 2024 00:45:18 +0800 Subject: [PATCH 022/192] Fix linting --- backend/code-execution-service/package.json | 1 + .../code-execution-service/src/utils/oneCompilerApi.ts | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/code-execution-service/package.json b/backend/code-execution-service/package.json index 9062357cf3..984729f9e0 100644 --- a/backend/code-execution-service/package.json +++ b/backend/code-execution-service/package.json @@ -2,6 +2,7 @@ "name": "code-execution-service", "version": "1.0.0", "main": "server.ts", + "type": "module", "scripts": { "start": "tsx server.ts", "dev": "tsx watch server.ts", diff --git a/backend/code-execution-service/src/utils/oneCompilerApi.ts b/backend/code-execution-service/src/utils/oneCompilerApi.ts index 3961346637..143ecacb2f 100644 --- a/backend/code-execution-service/src/utils/oneCompilerApi.ts +++ b/backend/code-execution-service/src/utils/oneCompilerApi.ts @@ -4,14 +4,14 @@ import dotenv from "dotenv"; dotenv.config(); interface FileType { - name: String; - content: String; + name: string; + content: string; } export const oneCompilerApi = async ( - language: String, - stdin: String, - userCode: String + language: string, + stdin: string, + userCode: string ) => { let files: FileType[] = []; if (language === "python") { From 15ded488571695f0d750ed9afb48a2d74d23723a Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Mon, 28 Oct 2024 00:53:59 +0800 Subject: [PATCH 023/192] Console log error --- .../src/controllers/codeExecutionControllers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index 6df09b4861..f84aa361e2 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -80,7 +80,8 @@ export const executeCode = async (req: Request, res: Response) => { message: SUCCESS_MESSAGE, data, }); - } catch { + } catch (err) { + console.log(err); res.status(500).json({ message: ERROR_FAILED_TO_EXECUTE_MESSAGE }); } }; From d7f866f05470cd03817060e8bb7b78b87dae2f1f Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Mon, 28 Oct 2024 00:59:16 +0800 Subject: [PATCH 024/192] Update docker compose test --- docker-compose-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose-test.yml b/docker-compose-test.yml index f3cabee5d6..7e5ea5e7df 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -89,6 +89,7 @@ services: environment: - NODE_ENV=test - SERVICE_PORT=3004 + - ONE_COMPILER_KEY networks: - peerprep-network volumes: From 14e5f0d69e8981926ecf5fc90872adfaf35a521e Mon Sep 17 00:00:00 2001 From: jolynloh Date: Mon, 28 Oct 2024 12:12:50 +0800 Subject: [PATCH 025/192] Add end session confirmation modal --- .../CollabSessionControls/index.tsx | 7 +- frontend/src/contexts/MatchContext.tsx | 24 +++++++ frontend/src/pages/CollabSandbox/index.tsx | 70 +++++++++++++++++-- 3 files changed, 92 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index a5ff015224..88bc3115f5 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -25,7 +25,7 @@ const CollabSessionControls: React.FC = () => { if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { stopMatch } = match; + const { handleEndSessionClick } = match; return ( @@ -38,7 +38,6 @@ const CollabSessionControls: React.FC = () => { variant="outlined" color="success" onClick={() => { - stopMatch(); console.log( `Time taken: ${extractHoursFromTime( time @@ -46,7 +45,7 @@ const CollabSessionControls: React.FC = () => { time )} mins ${extractSecondsFromTime(time)} secs` ); - }} // TODO: change to submit function with time taken pop-up + }} // TODO: implement submit function with time taken pop-up > Submit @@ -57,7 +56,7 @@ const CollabSessionControls: React.FC = () => { }} variant="outlined" color="error" - onClick={() => stopMatch()} + onClick={() => handleEndSessionClick()} > End Session diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 9c28b9e2f0..75bf7be3df 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -82,11 +82,15 @@ type MatchContextType = { matchOfferTimeout: () => void; verifyMatchStatus: () => void; getMatchId: () => string | null; + handleEndSessionClick: () => void; + handleRejectEndSession: () => void; + handleConfirmEndSession: () => void; matchUser: MatchUser | null; matchCriteria: MatchCriteria | null; partner: MatchUser | null; matchPending: boolean; loading: boolean; + isEndSessionModalOpen: boolean; }; const requestTimeoutDuration = 5000; @@ -112,6 +116,9 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [matchPending, setMatchPending] = useState(false); const [loading, setLoading] = useState(true); + const [isEndSessionModalOpen, setIsEndSessionModalOpen] = + useState(false); + const navigator = useContext(UNSAFE_NavigationContext).navigator as History; useEffect(() => { @@ -481,6 +488,19 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { return matchId; }; + const handleEndSessionClick = () => { + setIsEndSessionModalOpen(true); + } + + const handleRejectEndSession = () => { + setIsEndSessionModalOpen(false); + }; + + const handleConfirmEndSession = () => { + stopMatch(); + setIsEndSessionModalOpen(false); + }; + return ( = (props) => { matchOfferTimeout, verifyMatchStatus, getMatchId, + handleEndSessionClick, + handleRejectEndSession, + handleConfirmEndSession, matchUser, matchCriteria, partner, matchPending, loading, + isEndSessionModalOpen, }} > {children} diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 15cfc20039..baf455f019 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -1,5 +1,14 @@ import AppMargin from "../../components/AppMargin"; -import { Button, Stack, Typography } from "@mui/material"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Stack, + Typography, +} from "@mui/material"; import classes from "./index.module.css"; import { useMatch } from "../../contexts/MatchContext"; import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; @@ -12,7 +21,15 @@ const CollabSandbox: React.FC = () => { if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { stopMatch, verifyMatchStatus, getMatchId, partner, loading } = match; + const { + verifyMatchStatus, + getMatchId, + handleRejectEndSession, + handleConfirmEndSession, + partner, + loading, + isEndSessionModalOpen, + } = match; useEffect(() => { verifyMatchStatus(); @@ -41,10 +58,53 @@ const CollabSandbox: React.FC = () => { Successfully matched! - + + + {"End Session?"} + + + + Are you sure you want to end session? +
+ You will lose your current progress. +
+
+ + + + +
); }; From f79e30ceec53760bb21076aee1be6426422307a4 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Mon, 28 Oct 2024 13:46:06 +0800 Subject: [PATCH 026/192] Fix email verification routes --- frontend/src/App.tsx | 8 ++++---- frontend/src/contexts/AuthContext.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 78d1b8a4cc..15f1fd5095 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -69,10 +69,10 @@ function App() { } /> } /> } /> - } - /> + + } /> + } /> + } /> } /> diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index e618acf404..08665f8d2e 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -66,7 +66,7 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }) .then(() => userClient.post("users/send-verification-email", { email })) .then((res) => { - navigate(`/auth/verifyEmail/${res.data.data.id}`); + navigate(`/auth/verify-email/${res.data.data.id}`); }) .catch((err) => { setUser(null); @@ -88,7 +88,7 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }) .catch((err) => { if (err.response?.data.message === "User not verified.") { - navigate(`/auth/verifyEmail`); + navigate(`/auth/verify-email`); } setUser(null); toast.error(err.response?.data.message || err.message); From 593c45032b5e547a7bf02664465a04dcdc112582 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Mon, 28 Oct 2024 13:52:39 +0800 Subject: [PATCH 027/192] Remove duplicate button --- frontend/src/pages/EmailVerification/index.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/frontend/src/pages/EmailVerification/index.tsx b/frontend/src/pages/EmailVerification/index.tsx index 93174aa0b0..76d5f46184 100644 --- a/frontend/src/pages/EmailVerification/index.tsx +++ b/frontend/src/pages/EmailVerification/index.tsx @@ -77,7 +77,7 @@ const EmailVerification: React.FC = () => { onChange={(e) => setEmail(e.target.value)} slotProps={{ input: { - endAdornment: , + endAdornment: , }, }} /> @@ -93,14 +93,6 @@ const EmailVerification: React.FC = () => { spacing={2} sx={(theme) => ({ marginTop: theme.spacing(4) })} > - From 9ec9c0edb2cc752b0eb545b89c55fe90b39d7b95 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Mon, 28 Oct 2024 17:55:53 +0800 Subject: [PATCH 028/192] Shift code into src --- backend/user-service/package.json | 4 ++-- backend/user-service/{ => src}/app.ts | 0 backend/user-service/{ => src}/config/firebase.ts | 0 backend/user-service/{ => src}/config/multer.ts | 0 backend/user-service/{ => src}/config/redis.ts | 0 .../user-service/{ => src}/controller/auth-controller.ts | 0 .../user-service/{ => src}/controller/user-controller.ts | 0 .../{ => src}/middleware/basic-access-control.ts | 0 backend/user-service/{ => src}/model/repository.ts | 0 backend/user-service/{ => src}/model/user-model.ts | 0 backend/user-service/{ => src}/routes/auth-routes.ts | 0 backend/user-service/{ => src}/routes/user-routes.ts | 0 backend/user-service/{ => src}/scripts/seed.ts | 0 backend/user-service/{ => src}/server.ts | 2 +- backend/user-service/{ => src}/types/request.d.ts | 0 backend/user-service/{ => src}/utils/constants.ts | 0 backend/user-service/{ => src}/utils/mailer.ts | 0 backend/user-service/{ => src}/utils/utils.ts | 0 backend/user-service/{ => src}/utils/validators.ts | 0 backend/user-service/tests/authRoutes.spec.ts | 4 ++-- backend/user-service/tests/setup.ts | 2 +- backend/user-service/tests/userRoutes.spec.ts | 6 +++--- 22 files changed, 9 insertions(+), 9 deletions(-) rename backend/user-service/{ => src}/app.ts (100%) rename backend/user-service/{ => src}/config/firebase.ts (100%) rename backend/user-service/{ => src}/config/multer.ts (100%) rename backend/user-service/{ => src}/config/redis.ts (100%) rename backend/user-service/{ => src}/controller/auth-controller.ts (100%) rename backend/user-service/{ => src}/controller/user-controller.ts (100%) rename backend/user-service/{ => src}/middleware/basic-access-control.ts (100%) rename backend/user-service/{ => src}/model/repository.ts (100%) rename backend/user-service/{ => src}/model/user-model.ts (100%) rename backend/user-service/{ => src}/routes/auth-routes.ts (100%) rename backend/user-service/{ => src}/routes/user-routes.ts (100%) rename backend/user-service/{ => src}/scripts/seed.ts (100%) rename backend/user-service/{ => src}/server.ts (92%) rename backend/user-service/{ => src}/types/request.d.ts (100%) rename backend/user-service/{ => src}/utils/constants.ts (100%) rename backend/user-service/{ => src}/utils/mailer.ts (100%) rename backend/user-service/{ => src}/utils/utils.ts (100%) rename backend/user-service/{ => src}/utils/validators.ts (100%) diff --git a/backend/user-service/package.json b/backend/user-service/package.json index 4c192f9dc3..0cdabebd0f 100644 --- a/backend/user-service/package.json +++ b/backend/user-service/package.json @@ -5,8 +5,8 @@ "main": "app.ts", "type": "module", "scripts": { - "start": "tsx server.ts", - "dev": "tsx watch server.ts", + "start": "tsx ./src/server.ts", + "dev": "tsx watch ./src/server.ts", "lint": "eslint .", "test": "cross-env NODE_ENV=test jest --detectOpenHandles", "test:watch": "cross-env NODE_ENV=test jest --watch --detectOpenHandles" diff --git a/backend/user-service/app.ts b/backend/user-service/src/app.ts similarity index 100% rename from backend/user-service/app.ts rename to backend/user-service/src/app.ts diff --git a/backend/user-service/config/firebase.ts b/backend/user-service/src/config/firebase.ts similarity index 100% rename from backend/user-service/config/firebase.ts rename to backend/user-service/src/config/firebase.ts diff --git a/backend/user-service/config/multer.ts b/backend/user-service/src/config/multer.ts similarity index 100% rename from backend/user-service/config/multer.ts rename to backend/user-service/src/config/multer.ts diff --git a/backend/user-service/config/redis.ts b/backend/user-service/src/config/redis.ts similarity index 100% rename from backend/user-service/config/redis.ts rename to backend/user-service/src/config/redis.ts diff --git a/backend/user-service/controller/auth-controller.ts b/backend/user-service/src/controller/auth-controller.ts similarity index 100% rename from backend/user-service/controller/auth-controller.ts rename to backend/user-service/src/controller/auth-controller.ts diff --git a/backend/user-service/controller/user-controller.ts b/backend/user-service/src/controller/user-controller.ts similarity index 100% rename from backend/user-service/controller/user-controller.ts rename to backend/user-service/src/controller/user-controller.ts diff --git a/backend/user-service/middleware/basic-access-control.ts b/backend/user-service/src/middleware/basic-access-control.ts similarity index 100% rename from backend/user-service/middleware/basic-access-control.ts rename to backend/user-service/src/middleware/basic-access-control.ts diff --git a/backend/user-service/model/repository.ts b/backend/user-service/src/model/repository.ts similarity index 100% rename from backend/user-service/model/repository.ts rename to backend/user-service/src/model/repository.ts diff --git a/backend/user-service/model/user-model.ts b/backend/user-service/src/model/user-model.ts similarity index 100% rename from backend/user-service/model/user-model.ts rename to backend/user-service/src/model/user-model.ts diff --git a/backend/user-service/routes/auth-routes.ts b/backend/user-service/src/routes/auth-routes.ts similarity index 100% rename from backend/user-service/routes/auth-routes.ts rename to backend/user-service/src/routes/auth-routes.ts diff --git a/backend/user-service/routes/user-routes.ts b/backend/user-service/src/routes/user-routes.ts similarity index 100% rename from backend/user-service/routes/user-routes.ts rename to backend/user-service/src/routes/user-routes.ts diff --git a/backend/user-service/scripts/seed.ts b/backend/user-service/src/scripts/seed.ts similarity index 100% rename from backend/user-service/scripts/seed.ts rename to backend/user-service/src/scripts/seed.ts diff --git a/backend/user-service/server.ts b/backend/user-service/src/server.ts similarity index 92% rename from backend/user-service/server.ts rename to backend/user-service/src/server.ts index 78b033c119..606e155603 100644 --- a/backend/user-service/server.ts +++ b/backend/user-service/src/server.ts @@ -1,7 +1,7 @@ import http from "http"; import index from "./app.ts"; import dotenv from "dotenv"; -import { connectToDB } from "./model/repository"; +import { connectToDB } from "./model/repository.ts"; import { seedAdminAccount } from "./scripts/seed.ts"; import { connectRedis } from "./config/redis.ts"; diff --git a/backend/user-service/types/request.d.ts b/backend/user-service/src/types/request.d.ts similarity index 100% rename from backend/user-service/types/request.d.ts rename to backend/user-service/src/types/request.d.ts diff --git a/backend/user-service/utils/constants.ts b/backend/user-service/src/utils/constants.ts similarity index 100% rename from backend/user-service/utils/constants.ts rename to backend/user-service/src/utils/constants.ts diff --git a/backend/user-service/utils/mailer.ts b/backend/user-service/src/utils/mailer.ts similarity index 100% rename from backend/user-service/utils/mailer.ts rename to backend/user-service/src/utils/mailer.ts diff --git a/backend/user-service/utils/utils.ts b/backend/user-service/src/utils/utils.ts similarity index 100% rename from backend/user-service/utils/utils.ts rename to backend/user-service/src/utils/utils.ts diff --git a/backend/user-service/utils/validators.ts b/backend/user-service/src/utils/validators.ts similarity index 100% rename from backend/user-service/utils/validators.ts rename to backend/user-service/src/utils/validators.ts diff --git a/backend/user-service/tests/authRoutes.spec.ts b/backend/user-service/tests/authRoutes.spec.ts index 0951730310..044d45f900 100644 --- a/backend/user-service/tests/authRoutes.spec.ts +++ b/backend/user-service/tests/authRoutes.spec.ts @@ -1,8 +1,8 @@ import bcrypt from "bcrypt"; import { faker } from "@faker-js/faker"; import supertest from "supertest"; -import app from "../app"; -import UserModel from "../model/user-model"; +import app from "../src/app"; +import UserModel from "../src/model/user-model"; jest.setTimeout(10000); diff --git a/backend/user-service/tests/setup.ts b/backend/user-service/tests/setup.ts index a85ae2e02f..2916bfcd4b 100644 --- a/backend/user-service/tests/setup.ts +++ b/backend/user-service/tests/setup.ts @@ -1,5 +1,5 @@ import mongoose from "mongoose"; -import redisClient from "../config/redis"; +import redisClient from "../src/config/redis"; beforeAll(async () => { const mongoUri = diff --git a/backend/user-service/tests/userRoutes.spec.ts b/backend/user-service/tests/userRoutes.spec.ts index 9ff12dc3ea..81cdad43da 100644 --- a/backend/user-service/tests/userRoutes.spec.ts +++ b/backend/user-service/tests/userRoutes.spec.ts @@ -2,8 +2,8 @@ import bcrypt from "bcrypt"; import mongoose from "mongoose"; import { faker } from "@faker-js/faker"; import supertest from "supertest"; -import app from "../app"; -import UserModel from "../model/user-model"; +import app from "../src/app"; +import UserModel from "../src/model/user-model"; const request = supertest(app); @@ -13,7 +13,7 @@ faker.seed(0); const mockSendMail = jest.fn(); -jest.mock("../middleware/basic-access-control", () => ({ +jest.mock("../src/middleware/basic-access-control", () => ({ verifyAccessToken: jest.fn((req, res, next) => { req.user = { id: new mongoose.Types.ObjectId().toHexString(), From 5d72e9f843e74d7763b228e943a83285f1747ad9 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Mon, 28 Oct 2024 18:04:13 +0800 Subject: [PATCH 029/192] Shift code into src --- backend/question-service/package.json | 4 ++-- backend/question-service/{ => src}/app.ts | 2 +- backend/question-service/{ => src}/config/db.ts | 0 backend/question-service/{ => src}/config/firebase.ts | 0 backend/question-service/{ => src}/config/multer.ts | 0 .../question-service/src/controllers/questionController.ts | 2 +- backend/question-service/src/scripts/seed.ts | 2 +- backend/question-service/{ => src}/server.ts | 0 backend/question-service/src/utils/utils.ts | 6 ++++-- backend/question-service/tests/questionRoutes.spec.ts | 2 +- 10 files changed, 10 insertions(+), 8 deletions(-) rename backend/question-service/{ => src}/app.ts (96%) rename backend/question-service/{ => src}/config/db.ts (100%) rename backend/question-service/{ => src}/config/firebase.ts (100%) rename backend/question-service/{ => src}/config/multer.ts (100%) rename backend/question-service/{ => src}/server.ts (100%) diff --git a/backend/question-service/package.json b/backend/question-service/package.json index 9bdfda8e2e..16bd165501 100644 --- a/backend/question-service/package.json +++ b/backend/question-service/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "seed": "tsx src/scripts/seed.ts", - "start": "tsx server.ts", - "dev": "tsx watch server.ts", + "start": "tsx src/server.ts", + "dev": "tsx watch src/server.ts", "test": "cross-env NODE_ENV=test && jest", "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." diff --git a/backend/question-service/app.ts b/backend/question-service/src/app.ts similarity index 96% rename from backend/question-service/app.ts rename to backend/question-service/src/app.ts index 594545818f..86066cbe45 100644 --- a/backend/question-service/app.ts +++ b/backend/question-service/src/app.ts @@ -5,7 +5,7 @@ import yaml from "yaml"; import fs from "fs"; import cors from "cors"; -import questionRoutes from "./src/routes/questionRoutes.ts"; +import questionRoutes from "./routes/questionRoutes.ts"; dotenv.config(); diff --git a/backend/question-service/config/db.ts b/backend/question-service/src/config/db.ts similarity index 100% rename from backend/question-service/config/db.ts rename to backend/question-service/src/config/db.ts diff --git a/backend/question-service/config/firebase.ts b/backend/question-service/src/config/firebase.ts similarity index 100% rename from backend/question-service/config/firebase.ts rename to backend/question-service/src/config/firebase.ts diff --git a/backend/question-service/config/multer.ts b/backend/question-service/src/config/multer.ts similarity index 100% rename from backend/question-service/config/multer.ts rename to backend/question-service/src/config/multer.ts diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 1d01443cbc..10ece6e04e 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -17,7 +17,7 @@ import { MONGO_OBJ_ID_MALFORMED_MESSAGE, } from "../utils/constants.ts"; -import { upload } from "../../config/multer"; +import { upload } from "../config/multer.ts"; import { uploadFileToFirebase } from "../utils/utils"; export const createQuestion = async ( diff --git a/backend/question-service/src/scripts/seed.ts b/backend/question-service/src/scripts/seed.ts index 16929ab792..a2cf05e2f7 100644 --- a/backend/question-service/src/scripts/seed.ts +++ b/backend/question-service/src/scripts/seed.ts @@ -1,5 +1,5 @@ import { exit } from "process"; -import connectDB from "../../config/db"; +import connectDB from "../config/db"; import Question from "../models/Question"; export async function seedQuestions() { diff --git a/backend/question-service/server.ts b/backend/question-service/src/server.ts similarity index 100% rename from backend/question-service/server.ts rename to backend/question-service/src/server.ts diff --git a/backend/question-service/src/utils/utils.ts b/backend/question-service/src/utils/utils.ts index f0f5286f64..8e93776d23 100644 --- a/backend/question-service/src/utils/utils.ts +++ b/backend/question-service/src/utils/utils.ts @@ -1,7 +1,7 @@ import mongoose from "mongoose"; import { v4 as uuidv4 } from "uuid"; -import { bucket } from "../../config/firebase"; +import { bucket } from "../config/firebase"; import Question from "../models/Question"; @@ -50,5 +50,7 @@ export const uploadFileToFirebase = async ( }; export const sortAlphabetically = (arr: string[]) => { - return [...arr].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })); + return [...arr].sort((a, b) => + a.localeCompare(b, undefined, { sensitivity: "base" }), + ); }; diff --git a/backend/question-service/tests/questionRoutes.spec.ts b/backend/question-service/tests/questionRoutes.spec.ts index e538f57458..5e59c20ccd 100644 --- a/backend/question-service/tests/questionRoutes.spec.ts +++ b/backend/question-service/tests/questionRoutes.spec.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response } from "express"; import { faker } from "@faker-js/faker"; import supertest from "supertest"; -import app from "../app"; +import app from "../src/app"; import Question from "../src/models/Question"; import { DUPLICATE_QUESTION_MESSAGE, From d9190c46aeb71f91121bf2b02ce3c1d34f1bc4ee Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Mon, 28 Oct 2024 18:14:41 +0800 Subject: [PATCH 030/192] Shift code into src --- backend/matching-service/package.json | 4 ++-- backend/matching-service/{ => src}/app.ts | 2 +- backend/matching-service/{ => src}/config/rabbitmq.ts | 6 +++--- backend/matching-service/src/handlers/matchHandler.ts | 2 +- backend/matching-service/src/handlers/websocketHandler.ts | 2 +- backend/matching-service/{ => src}/server.ts | 2 +- backend/matching-service/src/utils/mq_utils.ts | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) rename backend/matching-service/{ => src}/app.ts (93%) rename backend/matching-service/{ => src}/config/rabbitmq.ts (90%) rename backend/matching-service/{ => src}/server.ts (91%) diff --git a/backend/matching-service/package.json b/backend/matching-service/package.json index eff673d2ff..c3648f2f53 100644 --- a/backend/matching-service/package.json +++ b/backend/matching-service/package.json @@ -4,8 +4,8 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx server.ts", - "dev": "tsx watch server.ts", + "start": "tsx src/server.ts", + "dev": "tsx watch src/server.ts", "test": "cross-env NODE_ENV=test && jest", "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." diff --git a/backend/matching-service/app.ts b/backend/matching-service/src/app.ts similarity index 93% rename from backend/matching-service/app.ts rename to backend/matching-service/src/app.ts index 7ebe04c04e..8eb5924f6b 100644 --- a/backend/matching-service/app.ts +++ b/backend/matching-service/src/app.ts @@ -5,7 +5,7 @@ import yaml from "yaml"; import fs from "fs"; import cors from "cors"; -import matchingRoutes from "./src/routes/matchingRoutes.ts"; +import matchingRoutes from "./routes/matchingRoutes.ts"; dotenv.config(); diff --git a/backend/matching-service/config/rabbitmq.ts b/backend/matching-service/src/config/rabbitmq.ts similarity index 90% rename from backend/matching-service/config/rabbitmq.ts rename to backend/matching-service/src/config/rabbitmq.ts index 03a84d8f15..9841e3808b 100644 --- a/backend/matching-service/config/rabbitmq.ts +++ b/backend/matching-service/src/config/rabbitmq.ts @@ -1,8 +1,8 @@ import amqplib, { Connection } from "amqplib"; import dotenv from "dotenv"; -import { matchUsers } from "../src/utils/mq_utils"; -import { MatchRequestItem } from "../src/handlers/matchHandler"; -import { Complexities, Categories, Languages } from "../src/utils/constants"; +import { matchUsers } from "../utils/mq_utils"; +import { MatchRequestItem } from "../handlers/matchHandler"; +import { Complexities, Categories, Languages } from "../utils/constants"; dotenv.config(); diff --git a/backend/matching-service/src/handlers/matchHandler.ts b/backend/matching-service/src/handlers/matchHandler.ts index e80b1748fb..2174fdea6c 100644 --- a/backend/matching-service/src/handlers/matchHandler.ts +++ b/backend/matching-service/src/handlers/matchHandler.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from "uuid"; -import { sendToQueue } from "../../config/rabbitmq"; +import { sendToQueue } from "../config/rabbitmq"; import { sendMatchFound } from "./websocketHandler"; interface Match { diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index 4f705777a9..47843f26dd 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -8,7 +8,7 @@ import { MatchUser, getMatchByUid, } from "./matchHandler"; -import { io } from "../../server"; +import { io } from "../server"; import { v4 as uuidv4 } from "uuid"; enum MatchEvents { diff --git a/backend/matching-service/server.ts b/backend/matching-service/src/server.ts similarity index 91% rename from backend/matching-service/server.ts rename to backend/matching-service/src/server.ts index fdc1933659..fc627d78ca 100644 --- a/backend/matching-service/server.ts +++ b/backend/matching-service/src/server.ts @@ -1,6 +1,6 @@ import http from "http"; import app, { allowedOrigins } from "./app.ts"; -import { handleWebsocketMatchEvents } from "./src/handlers/websocketHandler.ts"; +import { handleWebsocketMatchEvents } from "./handlers/websocketHandler.ts"; import { Server } from "socket.io"; import { connectToRabbitMq } from "./config/rabbitmq.ts"; diff --git a/backend/matching-service/src/utils/mq_utils.ts b/backend/matching-service/src/utils/mq_utils.ts index f1082a935e..457c4bd04e 100644 --- a/backend/matching-service/src/utils/mq_utils.ts +++ b/backend/matching-service/src/utils/mq_utils.ts @@ -1,4 +1,4 @@ -import { getPendingRequests } from "../../config/rabbitmq"; +import { getPendingRequests } from "../config/rabbitmq"; import { createMatch, MatchRequestItem } from "../handlers/matchHandler"; import { isActiveRequest, isUserConnected } from "../handlers/websocketHandler"; From 7df047d442618cf9ecfd28e0e575db86cfabfd2d Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Mon, 28 Oct 2024 18:23:29 +0800 Subject: [PATCH 031/192] Shift code into src --- backend/collab-service/package.json | 4 ++-- backend/collab-service/{ => src}/app.ts | 2 +- backend/collab-service/{ => src}/config/redis.ts | 0 backend/collab-service/src/handlers/websocketHandler.ts | 4 ++-- backend/collab-service/{ => src}/server.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename backend/collab-service/{ => src}/app.ts (94%) rename backend/collab-service/{ => src}/config/redis.ts (100%) rename backend/collab-service/{ => src}/server.ts (89%) diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index 9a3bf330e0..8660a0f62b 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -4,8 +4,8 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx server.ts", - "dev": "tsx watch server.ts", + "start": "tsx src/server.ts", + "dev": "tsx watch src/server.ts", "test": "cross-env NODE_ENV=test && jest", "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." diff --git a/backend/collab-service/app.ts b/backend/collab-service/src/app.ts similarity index 94% rename from backend/collab-service/app.ts rename to backend/collab-service/src/app.ts index 0ccce35d8a..9aaff3af65 100644 --- a/backend/collab-service/app.ts +++ b/backend/collab-service/src/app.ts @@ -5,7 +5,7 @@ import yaml from "yaml"; import swaggerUi from "swagger-ui-express"; import cors from "cors"; -import collabRoutes from "./src/routes/collabRoutes.ts"; +import collabRoutes from "./routes/collabRoutes.ts"; dotenv.config(); diff --git a/backend/collab-service/config/redis.ts b/backend/collab-service/src/config/redis.ts similarity index 100% rename from backend/collab-service/config/redis.ts rename to backend/collab-service/src/config/redis.ts diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 3a109d07f4..5a806a70d4 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -1,6 +1,6 @@ import { Socket } from "socket.io"; -import { io } from "../../server"; -import redisClient from "../../config/redis"; +import { io } from "../server"; +import redisClient from "../config/redis"; enum CollabEvents { // Receive diff --git a/backend/collab-service/server.ts b/backend/collab-service/src/server.ts similarity index 89% rename from backend/collab-service/server.ts rename to backend/collab-service/src/server.ts index c8ac91418f..d16a00c6ec 100644 --- a/backend/collab-service/server.ts +++ b/backend/collab-service/src/server.ts @@ -1,6 +1,6 @@ import http from "http"; import app, { allowedOrigins } from "./app.ts"; -import { handleWebsocketCollabEvents } from "./src/handlers/websocketHandler"; +import { handleWebsocketCollabEvents } from "./handlers/websocketHandler.ts"; import { Server } from "socket.io"; import { connectRedis } from "./config/redis.ts"; From afccc14535a4a527332f8afbbb84a4c4b83334db Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Tue, 29 Oct 2024 00:11:46 +0800 Subject: [PATCH 032/192] Implement websocket and chat layout --- backend/communication-service/.dockerignore | 5 + backend/communication-service/.env.sample | 4 + backend/communication-service/Dockerfile | 13 + .../communication-service/package-lock.json | 1667 +++++++++++++++++ backend/communication-service/package.json | 24 + backend/communication-service/src/app.ts | 21 + .../src/handlers/websocketHandler.ts | 72 + backend/communication-service/src/server.ts | 21 + .../src/utils/constants.ts | 1 + .../communication-service/src/utils/types.ts | 13 + docker-compose.yml | 15 + frontend/package-lock.json | 227 ++- frontend/package.json | 1 + frontend/src/components/Chat/index.tsx | 24 + frontend/src/pages/CollabSandbox/index.tsx | 52 +- frontend/src/utils/communicationSocket.ts | 8 + 16 files changed, 2110 insertions(+), 58 deletions(-) create mode 100644 backend/communication-service/.dockerignore create mode 100644 backend/communication-service/.env.sample create mode 100644 backend/communication-service/Dockerfile create mode 100644 backend/communication-service/package-lock.json create mode 100644 backend/communication-service/package.json create mode 100644 backend/communication-service/src/app.ts create mode 100644 backend/communication-service/src/handlers/websocketHandler.ts create mode 100644 backend/communication-service/src/server.ts create mode 100644 backend/communication-service/src/utils/constants.ts create mode 100644 backend/communication-service/src/utils/types.ts create mode 100644 frontend/src/components/Chat/index.tsx create mode 100644 frontend/src/utils/communicationSocket.ts diff --git a/backend/communication-service/.dockerignore b/backend/communication-service/.dockerignore new file mode 100644 index 0000000000..4abc77f632 --- /dev/null +++ b/backend/communication-service/.dockerignore @@ -0,0 +1,5 @@ +coverage +node_modules +tests +.env* +*.md diff --git a/backend/communication-service/.env.sample b/backend/communication-service/.env.sample new file mode 100644 index 0000000000..07d3ce1596 --- /dev/null +++ b/backend/communication-service/.env.sample @@ -0,0 +1,4 @@ +NODE_ENV=development +SERVER_PORT=3005 + +ORIGINS=http://localhost:5173,http://127.0.0.1:5173 \ No newline at end of file diff --git a/backend/communication-service/Dockerfile b/backend/communication-service/Dockerfile new file mode 100644 index 0000000000..e38cd4dca4 --- /dev/null +++ b/backend/communication-service/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /communication-service + +COPY package*.json ./ + +RUN npm ci + +COPY . . + +EXPOSE 3005 + +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/backend/communication-service/package-lock.json b/backend/communication-service/package-lock.json new file mode 100644 index 0000000000..5c078d2108 --- /dev/null +++ b/backend/communication-service/package-lock.json @@ -0,0 +1,1667 @@ +{ + "name": "communication-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "communication-service", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "socket.io": "^4.8.1" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/socket.io": "^3.0.1", + "tsx": "^4.19.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, + "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/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", + "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/socket.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", + "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, + "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/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "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/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.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/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 + } + } + } + } +} diff --git a/backend/communication-service/package.json b/backend/communication-service/package.json new file mode 100644 index 0000000000..9d89c84480 --- /dev/null +++ b/backend/communication-service/package.json @@ -0,0 +1,24 @@ +{ + "name": "communication-service", + "version": "1.0.0", + "main": "server.ts", + "scripts": { + "start": "tsx src/server.ts", + "dev": "tsx watch src/server.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "socket.io": "^4.8.1" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/socket.io": "^3.0.1", + "tsx": "^4.19.2" + } +} diff --git a/backend/communication-service/src/app.ts b/backend/communication-service/src/app.ts new file mode 100644 index 0000000000..1c1895e7f8 --- /dev/null +++ b/backend/communication-service/src/app.ts @@ -0,0 +1,21 @@ +import express, { Request, Response } from "express"; +import dotenv from "dotenv"; +import cors from "cors"; + +dotenv.config(); + +const app = express(); + +export const allowedOrigins = process.env.ORIGINS + ? process.env.ORIGINS.split(",") + : ["http://localhost:5173", "http://127.0.0.1:5173"]; + +app.use(cors({ origin: allowedOrigins, credentials: true })); + +app.options("*", cors({ origin: allowedOrigins, credentials: true })); + +app.get("/", (req: Request, res: Response) => { + res.status(200).json({ message: "Hello world from communication service" }); +}); + +export default app; diff --git a/backend/communication-service/src/handlers/websocketHandler.ts b/backend/communication-service/src/handlers/websocketHandler.ts new file mode 100644 index 0000000000..c4857d74fc --- /dev/null +++ b/backend/communication-service/src/handlers/websocketHandler.ts @@ -0,0 +1,72 @@ +import { Socket } from "socket.io"; +import { CommunicationEvents } from "../utils/types"; +import { BOT_NAME } from "../utils/constants"; +import { io } from "../server"; + +export const handleWebsocketCommunicationEvents = (socket: Socket) => { + socket.on( + CommunicationEvents.JOIN, + async ({ roomId, username }: { roomId: string; username: string }) => { + if (!roomId) { + return; + } + + socket.join(roomId); + socket.data.roomId = roomId; + + // send the message to all users (including the sender) in the room + const createdTime = Date.now(); + io.to(roomId).emit(CommunicationEvents.USER_JOINED, { + from: BOT_NAME, + message: `${username} has joined the chat`, + createdTime, + }); + } + ); + + socket.on( + CommunicationEvents.LEAVE, + ({ roomId, username }: { roomId: string; username: string }) => { + if (!roomId) { + return; + } + + socket.leave(roomId); + const createdTime = Date.now(); + socket.to(roomId).emit(CommunicationEvents.LEAVE, { + from: BOT_NAME, + message: `${username} has left the chat`, + createdTime, + }); + } + ); + + socket.on( + CommunicationEvents.SEND_TEXT_MESSAGE, + ({ + roomId, + message, + username, + createdTime, + }: { + roomId: string; + message: string; + username: string; + createdTime: number; + }) => { + // send the message to all users (including the sender) in the room + io.to(roomId).emit(CommunicationEvents.TEXT_MESSAGE_RECEIVED, { + from: username, + message, + createdTime, + }); + } + ); + + socket.on(CommunicationEvents.DISCONNECT, () => { + const { roomId } = socket.data; + if (roomId) { + socket.to(roomId).emit(CommunicationEvents.DISCONNECTED); + } + }); +}; diff --git a/backend/communication-service/src/server.ts b/backend/communication-service/src/server.ts new file mode 100644 index 0000000000..2ab4402245 --- /dev/null +++ b/backend/communication-service/src/server.ts @@ -0,0 +1,21 @@ +import app, { allowedOrigins } from "./app"; +import { createServer } from "http"; +import { Server } from "socket.io"; +import { handleWebsocketCommunicationEvents } from "./handlers/websocketHandler"; + +const PORT = process.env.SERVICE_PORT || 3005; + +const server = createServer(app); + +export const io = new Server(server, { + cors: { origin: allowedOrigins, methods: ["GET", "POST"] }, + connectionStateRecovery: {}, +}); + +io.on("connection", handleWebsocketCommunicationEvents); + +app.listen(PORT, () => { + console.log( + `Communication service server listening on port http://localhost:${PORT}` + ); +}); diff --git a/backend/communication-service/src/utils/constants.ts b/backend/communication-service/src/utils/constants.ts new file mode 100644 index 0000000000..f15ec8193b --- /dev/null +++ b/backend/communication-service/src/utils/constants.ts @@ -0,0 +1 @@ +export const BOT_NAME = "bot"; diff --git a/backend/communication-service/src/utils/types.ts b/backend/communication-service/src/utils/types.ts new file mode 100644 index 0000000000..140c5550e6 --- /dev/null +++ b/backend/communication-service/src/utils/types.ts @@ -0,0 +1,13 @@ +export enum CommunicationEvents { + // receive + JOIN = "join", + LEAVE = "leave", + SEND_TEXT_MESSAGE = "send_text_message", + DISCONNECT = "disconnect", + + // send + USER_LEFT = "user_left", + USER_JOINED = "user_joined", + TEXT_MESSAGE_RECEIVED = "text_message_received", + DISCONNECTED = "disconnected", +} diff --git a/docker-compose.yml b/docker-compose.yml index 629a715847..a339e9a3f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,6 +70,21 @@ services: - /collab-service/node_modules restart: on-failure + communication-service: + image: peerprep/communication-service + build: ./backend/communication-service + environment: + - CHOKIDAR_USEPOLLING=true + env_file: ./backend/communication-service/.env + ports: + - 3005:3005 + networks: + - peerprep-network + volumes: + - ./backend/communication-service:/communication-service + - /communication-service/node_modules + restart: on-failure + frontend: image: peerprep/frontend build: ./frontend diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5833bd7fd3..eaafd17673 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", "@mui/icons-material": "^6.1.0", + "@mui/lab": "^6.0.0-beta.13", "@mui/material": "^6.1.0", "@uiw/react-md-editor": "^4.0.4", "axios": "^1.7.7", @@ -1866,9 +1867,10 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2030,14 +2032,15 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.1.tgz", - "integrity": "sha512-dEPNKzBPU+vFPGa+z3axPRn8XVDetYORmDC0wAiej+TNcOZE70ZMJa0X7JdeoM6q/nWTMZeLpN/fTnD9o8MQBA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz", + "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==", + "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.0", + "@emotion/utils": "^1.4.1", "csstype": "^3.0.2" } }, @@ -2082,9 +2085,10 @@ } }, "node_modules/@emotion/utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz", - "integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz", + "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==", + "license": "MIT" }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", @@ -2553,6 +2557,44 @@ "npm": ">=9.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz", + "integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "license": "MIT" + }, "node_modules/@fontsource/roboto": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.1.0.tgz", @@ -3455,10 +3497,43 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.60", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.60.tgz", + "integrity": "sha512-w8twR3qCUI+uJHO5xDOuc1yB5l46KFbvNsTwIvEW9tQkKxVaiEFf2GAXHuvFJiHfZLqjzett6drZjghy8D1Z1A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@floating-ui/react-dom": "^2.1.1", + "@mui/types": "^7.2.18", + "@mui/utils": "^6.1.5", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.1.tgz", - "integrity": "sha512-VdQC1tPIIcZAnf62L2M1eQif0x2vlKg3YK4kGYbtijSH4niEgI21GnstykW1vQIs+Bc6L+Hua2GATYVjilJ22A==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.5.tgz", + "integrity": "sha512-3J96098GrC95XsLw/TpGNMxhUOnoG9NZ/17Pfk1CrJj+4rcuolsF2RdF3XAFTu/3a/A+5ouxlSIykzYz6Ee87g==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" @@ -3489,16 +3564,62 @@ } } }, + "node_modules/@mui/lab": { + "version": "6.0.0-beta.13", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.13.tgz", + "integrity": "sha512-gLcAL96KhV1aA7sCaganPitVb+NT42Y2KsmnHmCtCVqAgBgSmC4D6mcH7MjjR1UAQt+DfxeeoqrFIQjKTI/wmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/base": "5.0.0-beta.60", + "@mui/system": "^6.1.5", + "@mui/types": "^7.2.18", + "@mui/utils": "^6.1.5", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": "^6.1.5", + "@mui/material-pigment-css": "^6.1.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.1.tgz", - "integrity": "sha512-b+eULldTqtqTCbN++2BtBWCir/1LwEYw+2mIlOt2GiEUh1EBBw4/wIukGKKNt3xrCZqRA80yLLkV6tF61Lq3cA==", - "dependencies": { - "@babel/runtime": "^7.25.6", - "@mui/core-downloads-tracker": "^6.1.1", - "@mui/system": "^6.1.1", - "@mui/types": "^7.2.17", - "@mui/utils": "^6.1.1", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.5.tgz", + "integrity": "sha512-rhaxC7LnlOG8zIVYv7BycNbWkC5dlm9A/tcDUp0CuwA7Zf9B9JP6M3rr50cNKxI7Z0GIUesAT86ceVm44quwnQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/core-downloads-tracker": "^6.1.5", + "@mui/system": "^6.1.5", + "@mui/types": "^7.2.18", + "@mui/utils": "^6.1.5", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", @@ -3517,7 +3638,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.1.1", + "@mui/material-pigment-css": "^6.1.5", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3538,12 +3659,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.1.tgz", - "integrity": "sha512-JlrjIdhyZUtewtdAuUsvi3ZnO0YS49IW4Mfz19ZWTlQ0sDGga6LNPVwHClWr2/zJK2we2BQx9/i8M32rgKuzrg==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.5.tgz", + "integrity": "sha512-FJqweqEXk0KdtTho9C2h6JEKXsOT7MAVH2Uj3N5oIqs6YKxnwBn2/zL2QuYYEtj5OJ87rEUnCfFic6ldClvzJw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.6", - "@mui/utils": "^6.1.1", + "@babel/runtime": "^7.25.7", + "@mui/utils": "^6.1.5", "prop-types": "^15.8.1" }, "engines": { @@ -3564,12 +3686,14 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.1.tgz", - "integrity": "sha512-HJyIoMpFb11fnHuRtUILOXgq6vj4LhIlE8maG4SwP/W+E5sa7HFexhnB3vOMT7bKys4UKNxhobC8jwWxYilGsA==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.5.tgz", + "integrity": "sha512-tiyWzMkHeWlOoE6AqomWvYvdml8Nv5k5T+LDwOiwHEawx8P9Lyja6ZwWPU6xljwPXYYPT2KBp1XvMly7dsK46A==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.6", + "@babel/runtime": "^7.25.7", "@emotion/cache": "^11.13.1", + "@emotion/serialize": "^1.3.2", "@emotion/sheet": "^1.4.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -3596,15 +3720,16 @@ } }, "node_modules/@mui/system": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.1.tgz", - "integrity": "sha512-PaYsCz2tUOcpu3T0okDEsSuP/yCDIj9JZ4Tox1JovRSKIjltHpXPsXZSGr3RiWdtM1MTQMFMCZzu0+CKbyy+Kw==", - "dependencies": { - "@babel/runtime": "^7.25.6", - "@mui/private-theming": "^6.1.1", - "@mui/styled-engine": "^6.1.1", - "@mui/types": "^7.2.17", - "@mui/utils": "^6.1.1", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.5.tgz", + "integrity": "sha512-vPM9ocQ8qquRDByTG3XF/wfYTL7IWL/20EiiKqByLDps8wOmbrDG9rVznSE3ZbcjFCFfMRMhtxvN92bwe/63SA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/private-theming": "^6.1.5", + "@mui/styled-engine": "^6.1.5", + "@mui/types": "^7.2.18", + "@mui/utils": "^6.1.5", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -3635,9 +3760,10 @@ } }, "node_modules/@mui/types": { - "version": "7.2.17", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.17.tgz", - "integrity": "sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==", + "version": "7.2.18", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.18.tgz", + "integrity": "sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==", + "license": "MIT", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -3648,13 +3774,14 @@ } }, "node_modules/@mui/utils": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.1.tgz", - "integrity": "sha512-HlRrgdJSPbYDXPpoVMWZV8AE7WcFtAk13rWNWAEVWKSanzBBkymjz3km+Th/Srowsh4pf1fTSP1B0L116wQBYw==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.5.tgz", + "integrity": "sha512-vp2WfNDY+IbKUIGg+eqX1Ry4t/BilMjzp6p9xO1rfqpYjH1mj8coQxxDfKxcQLzBQkmBJjymjoGOak5VUYwXug==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.6", - "@mui/types": "^7.2.17", - "@types/prop-types": "^15.7.12", + "@babel/runtime": "^7.25.7", + "@mui/types": "^7.2.18", + "@types/prop-types": "^15.7.13", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^18.3.1" diff --git a/frontend/package.json b/frontend/package.json index 7f0aba9259..829b474cb8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", "@mui/icons-material": "^6.1.0", + "@mui/lab": "^6.0.0-beta.13", "@mui/material": "^6.1.0", "@uiw/react-md-editor": "^4.0.4", "axios": "^1.7.7", diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx new file mode 100644 index 0000000000..2c04f93d3c --- /dev/null +++ b/frontend/src/components/Chat/index.tsx @@ -0,0 +1,24 @@ +import { Box, TextField } from "@mui/material"; + +const Chat: React.FC = () => { + return ( + + {/* Chat messages */} + + + + + ); +}; + +export default Chat; diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 7b328a4662..03ad3c8b6f 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -7,11 +7,13 @@ import { DialogContent, DialogContentText, DialogTitle, + Tab, } from "@mui/material"; +import { TabContext, TabList, TabPanel } from "@mui/lab"; import classes from "./index.module.css"; import { useMatch } from "../../contexts/MatchContext"; import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; -import { useEffect, useReducer } from "react"; +import { useEffect, useReducer, useState } from "react"; import Loader from "../../components/Loader"; import ServerError from "../../components/ServerError"; import reducer, { @@ -19,6 +21,7 @@ import reducer, { initialState, } from "../../reducers/questionReducer"; import QuestionDetailComponent from "../../components/QuestionDetail"; +import Chat from "../../components/Chat"; const CollabSandbox: React.FC = () => { const match = useMatch(); @@ -38,6 +41,7 @@ const CollabSandbox: React.FC = () => { } = match; const [state, dispatch] = useReducer(reducer, initialState); const { selectedQuestion } = state; + const [selectedTab, setSelectedTab] = useState<"tests" | "chat">("tests"); useEffect(() => { verifyMatchStatus(); @@ -69,9 +73,6 @@ const CollabSandbox: React.FC = () => { return ( - {/* - Successfully matched! - */} { + {/* Left side */} ({ flex: 1, marginRight: theme.spacing(2) })}> { categories={selectedQuestion.categories} /> - ({ flex: 1, marginLeft: theme.spacing(2) })}> + {/* Right side */} + ({ + flex: 1, + marginLeft: theme.spacing(2), + display: "flex", + flexDirection: "column", + })} + > + Code editor - Code editor - Test cases and chat tabs + + + setSelectedTab(value)}> + + + + + + Tests + + + + + diff --git a/frontend/src/utils/communicationSocket.ts b/frontend/src/utils/communicationSocket.ts new file mode 100644 index 0000000000..ebda964f0b --- /dev/null +++ b/frontend/src/utils/communicationSocket.ts @@ -0,0 +1,8 @@ +import { io } from "socket.io-client"; + +const COMMUNICATION_SOCKET_URL = "http://localhost:3005"; + +export const communicationSocket = io(COMMUNICATION_SOCKET_URL, { + reconnectionAttempts: 3, + autoConnect: false, +}); From 8d5562f621d6194fc04f218332509f251723c68c Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 29 Oct 2024 00:16:03 +0800 Subject: [PATCH 033/192] Fix Dockerfile --- backend/code-execution-service/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/code-execution-service/Dockerfile b/backend/code-execution-service/Dockerfile index 00840fdb06..3e6037c84b 100644 --- a/backend/code-execution-service/Dockerfile +++ b/backend/code-execution-service/Dockerfile @@ -8,6 +8,6 @@ RUN npm ci COPY . . -EXPOSE 3003 +EXPOSE 3004 CMD ["npm", "run", "dev"] From 9f350d21d691bd8d6b0ec77d95e22ff434484085 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Tue, 29 Oct 2024 02:52:19 +0800 Subject: [PATCH 034/192] Fix lint error and bugs in specific scenarios --- .../CollabSessionControls/index.tsx | 2 +- frontend/src/contexts/MatchContext.tsx | 5 +-- frontend/src/pages/CollabSandbox/index.tsx | 33 +++++++++++++++++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 88bc3115f5..3f261e0077 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -13,7 +13,7 @@ const CollabSessionControls: React.FC = () => { const [time, setTime] = useState(0); useEffect(() => { - let intervalId = setInterval( + const intervalId = setInterval( () => setTime((prevTime) => prevTime + 1), 1000 ); diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 57a4ecb1cd..c15a0f5e52 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -281,8 +281,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const initMatchedListeners = () => { matchSocket.on(MatchEvents.MATCH_SUCCESSFUL, (id: string) => { setMatchPending(false); - appNavigate(MatchPaths.COLLAB); setQuestionId(id); + appNavigate(MatchPaths.COLLAB); }); matchSocket.on(MatchEvents.MATCH_UNSUCCESSFUL, () => { @@ -302,6 +302,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const initCollabListeners = () => { matchSocket.on(MatchEvents.MATCH_ENDED, () => { toast.error(MATCH_ENDED_MESSAGE); + setIsEndSessionModalOpen(false); appNavigate(MatchPaths.HOME); }); }; @@ -501,8 +502,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const handleConfirmEndSession = () => { - stopMatch(); setIsEndSessionModalOpen(false); + stopMatch(); }; return ( diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 7b328a4662..274f762470 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -11,7 +11,7 @@ import { import classes from "./index.module.css"; import { useMatch } from "../../contexts/MatchContext"; import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; -import { useEffect, useReducer } from "react"; +import { useEffect, useReducer, useState } from "react"; import Loader from "../../components/Loader"; import ServerError from "../../components/ServerError"; import reducer, { @@ -19,8 +19,11 @@ import reducer, { initialState, } from "../../reducers/questionReducer"; import QuestionDetailComponent from "../../components/QuestionDetail"; +import { Navigate } from "react-router-dom"; const CollabSandbox: React.FC = () => { + const [showErrorScreen, setShowErrorScreen] = useState(false); + const match = useMatch(); if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); @@ -40,6 +43,10 @@ const CollabSandbox: React.FC = () => { const { selectedQuestion } = state; useEffect(() => { + if (!partner) { + return; + } + verifyMatchStatus(); if (!questionId) { @@ -54,11 +61,29 @@ const CollabSandbox: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + let timeout: number | undefined; + + if (!selectedQuestion) { + timeout = setTimeout(() => { + setShowErrorScreen(true); + }, 2000); + } else { + setShowErrorScreen(false); + } + + return () => clearTimeout(timeout); + }, [selectedQuestion]); + if (loading) { return ; } - if (!partner || !questionId || !selectedQuestion) { + if (!partner) { + return ; + } + + if (showErrorScreen) { return ( { ); } + if (!selectedQuestion) { + return ; + } + return ( {/* From 678d15e6d49f6a3a045faa2798dc895f9dbec528 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Tue, 29 Oct 2024 09:45:11 +0800 Subject: [PATCH 035/192] Enable chat on the frontend --- .../src/handlers/websocketHandler.ts | 14 +- backend/communication-service/src/server.ts | 2 +- .../communication-service/src/utils/types.ts | 6 + frontend/src/components/Chat/index.tsx | 166 +++++++++++++++++- 4 files changed, 183 insertions(+), 5 deletions(-) diff --git a/backend/communication-service/src/handlers/websocketHandler.ts b/backend/communication-service/src/handlers/websocketHandler.ts index c4857d74fc..07e3b46852 100644 --- a/backend/communication-service/src/handlers/websocketHandler.ts +++ b/backend/communication-service/src/handlers/websocketHandler.ts @@ -1,5 +1,5 @@ import { Socket } from "socket.io"; -import { CommunicationEvents } from "../utils/types"; +import { CommunicationEvents, MessageTypes } from "../utils/types"; import { BOT_NAME } from "../utils/constants"; import { io } from "../server"; @@ -11,6 +11,13 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { return; } + const room = io.sockets.adapter.rooms.get(roomId); + if (room?.has(socket.id)) { + // todo: fetch messages from cache and send to the user + socket.emit(CommunicationEvents.ALREADY_JOINED); + return; + } + socket.join(roomId); socket.data.roomId = roomId; @@ -18,6 +25,7 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { const createdTime = Date.now(); io.to(roomId).emit(CommunicationEvents.USER_JOINED, { from: BOT_NAME, + type: MessageTypes.BOT_GENERATED, message: `${username} has joined the chat`, createdTime, }); @@ -35,6 +43,7 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { const createdTime = Date.now(); socket.to(roomId).emit(CommunicationEvents.LEAVE, { from: BOT_NAME, + type: MessageTypes.BOT_GENERATED, message: `${username} has left the chat`, createdTime, }); @@ -57,9 +66,12 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { // send the message to all users (including the sender) in the room io.to(roomId).emit(CommunicationEvents.TEXT_MESSAGE_RECEIVED, { from: username, + type: MessageTypes.USER_GENERATED, message, createdTime, }); + + // todo: store the message in a cache } ); diff --git a/backend/communication-service/src/server.ts b/backend/communication-service/src/server.ts index 2ab4402245..d3a5e530bd 100644 --- a/backend/communication-service/src/server.ts +++ b/backend/communication-service/src/server.ts @@ -14,7 +14,7 @@ export const io = new Server(server, { io.on("connection", handleWebsocketCommunicationEvents); -app.listen(PORT, () => { +server.listen(PORT, () => { console.log( `Communication service server listening on port http://localhost:${PORT}` ); diff --git a/backend/communication-service/src/utils/types.ts b/backend/communication-service/src/utils/types.ts index 140c5550e6..ec56a8548f 100644 --- a/backend/communication-service/src/utils/types.ts +++ b/backend/communication-service/src/utils/types.ts @@ -8,6 +8,12 @@ export enum CommunicationEvents { // send USER_LEFT = "user_left", USER_JOINED = "user_joined", + ALREADY_JOINED = "already_joined", TEXT_MESSAGE_RECEIVED = "text_message_received", DISCONNECTED = "disconnected", } + +export enum MessageTypes { + USER_GENERATED = "user_generated", + BOT_GENERATED = "bot_generated", +} diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 2c04f93d3c..dadabde8ab 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -1,6 +1,84 @@ -import { Box, TextField } from "@mui/material"; +import { Box, TextField, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import { communicationSocket } from "../../utils/communicationSocket"; +import { useMatch } from "../../contexts/MatchContext"; +import { + USE_AUTH_ERROR_MESSAGE, + USE_MATCH_ERROR_MESSAGE, +} from "../../utils/constants"; +import { useAuth } from "../../contexts/AuthContext"; + +type Message = { + from: string; + type: "user_generated" | "bot_generated"; + message: string; + createdTime: number; +}; + +enum CommunicationEvents { + // receive + JOIN = "join", + LEAVE = "leave", + SEND_TEXT_MESSAGE = "send_text_message", + DISCONNECT = "disconnect", + + // send + USER_LEFT = "user_left", + USER_JOINED = "user_joined", + ALREADY_JOINED = "already_joined", + TEXT_MESSAGE_RECEIVED = "text_message_received", + DISCONNECTED = "disconnected", +} const Chat: React.FC = () => { + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(""); + const match = useMatch(); + const auth = useAuth(); + + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + + if (!auth) { + throw new Error(USE_AUTH_ERROR_MESSAGE); + } + + const { getMatchId } = match; + const { user } = auth; + + useEffect(() => { + // join the room automatically when this loads + communicationSocket.open(); + // to make sure this does not run twice + communicationSocket.emit(CommunicationEvents.JOIN, { + roomId: getMatchId(), + username: user?.username, + }); + // joinedRef.current = true; + }, []); + + useEffect(() => { + // initliase listerner for incoming messages + communicationSocket.on( + CommunicationEvents.USER_JOINED, + (message: Message) => { + setMessages((prevMessages) => [...prevMessages, message]); + } + ); + communicationSocket.on( + CommunicationEvents.TEXT_MESSAGE_RECEIVED, + (message: Message) => { + setMessages((prevMessages) => [...prevMessages, message]); + } + ); + + return () => { + communicationSocket.off(CommunicationEvents.USER_JOINED); + communicationSocket.off(CommunicationEvents.TEXT_MESSAGE_RECEIVED); + }; + }, []); + return ( { overflow: "auto", }} > - {/* Chat messages */} + + {messages.map((msg, id) => + msg.type === "bot_generated" ? ( + ({ + display: "flex", + justifyContent: "center", + margin: theme.spacing(1, 0), + })} + > + ({ + width: "fit-content", + color: theme.palette.secondary.contrastText, + background: theme.palette.secondary.main, + padding: theme.spacing(0.5, 1), + borderRadius: theme.spacing(1), + fontSize: "12px", + fontWeight: "bold", + })} + > + {msg.message} + + + ) : msg.from === user?.username ? ( + ({ + display: "flex", + justifyContent: "flex-end", + margin: theme.spacing(2), + })} + > + ({ + background: theme.palette.primary.main, + padding: theme.spacing(1), + borderRadius: theme.spacing(2), + maxWidth: "80%", + })} + > + {msg.message} + + + ) : ( + ({ + display: "flex", + justifyContent: "flex-start", + margin: theme.spacing(2), + maxWidth: "80%", + })} + > + ({ + background: theme.palette.secondary.main, + padding: theme.spacing(1), + borderRadius: theme.spacing(2), + })} + > + {msg.message} + + + ) + )} + - + setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + communicationSocket.emit(CommunicationEvents.SEND_TEXT_MESSAGE, { + roomId: getMatchId(), + message: inputValue, + username: user?.username, + createdTime: Date.now(), + }); + setInputValue(""); + } + }} + /> ); From 826965c4dac32beb0118aae284c48fd547a47361 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Tue, 29 Oct 2024 15:54:04 +0800 Subject: [PATCH 036/192] Communication service --- .../src/handlers/websocketHandler.ts | 9 +- frontend/src/components/Chat/index.tsx | 73 ++++++++------- frontend/src/components/TabPanel/index.tsx | 28 ++++++ frontend/src/pages/CollabSandbox/index.tsx | 89 +++++++++---------- 4 files changed, 115 insertions(+), 84 deletions(-) create mode 100644 frontend/src/components/TabPanel/index.tsx diff --git a/backend/communication-service/src/handlers/websocketHandler.ts b/backend/communication-service/src/handlers/websocketHandler.ts index 07e3b46852..4dd329fd6e 100644 --- a/backend/communication-service/src/handlers/websocketHandler.ts +++ b/backend/communication-service/src/handlers/websocketHandler.ts @@ -20,6 +20,7 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { socket.join(roomId); socket.data.roomId = roomId; + socket.data.username = username; // send the message to all users (including the sender) in the room const createdTime = Date.now(); @@ -78,7 +79,13 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { socket.on(CommunicationEvents.DISCONNECT, () => { const { roomId } = socket.data; if (roomId) { - socket.to(roomId).emit(CommunicationEvents.DISCONNECTED); + const createdTime = Date.now(); + socket.to(roomId).emit(CommunicationEvents.DISCONNECTED, { + from: BOT_NAME, + type: MessageTypes.BOT_GENERATED, + message: `${socket.data.username} has disconnected`, + createdTime, + }); } }); }; diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index dadabde8ab..f5f79de66c 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -72,6 +72,12 @@ const Chat: React.FC = () => { setMessages((prevMessages) => [...prevMessages, message]); } ); + communicationSocket.on( + CommunicationEvents.DISCONNECTED, + (message: Message) => { + setMessages((prevMessages) => [...prevMessages, message]); + } + ); return () => { communicationSocket.off(CommunicationEvents.USER_JOINED); @@ -79,17 +85,10 @@ const Chat: React.FC = () => { }; }, []); + console.log(messages); return ( - - + <> + {messages.map((msg, id) => msg.type === "bot_generated" ? ( { sx={(theme) => ({ display: "flex", justifyContent: "flex-end", - margin: theme.spacing(2), + marginTop: theme.spacing(1), })} > ({ background: theme.palette.primary.main, - padding: theme.spacing(1), + padding: theme.spacing(1, 2), borderRadius: theme.spacing(2), maxWidth: "80%", })} @@ -138,15 +137,15 @@ const Chat: React.FC = () => { sx={(theme) => ({ display: "flex", justifyContent: "flex-start", - margin: theme.spacing(2), - maxWidth: "80%", + marginTop: theme.spacing(1), })} > ({ background: theme.palette.secondary.main, - padding: theme.spacing(1), + padding: theme.spacing(1, 2), borderRadius: theme.spacing(2), + maxWidth: "80%", })} > {msg.message} @@ -155,29 +154,27 @@ const Chat: React.FC = () => { ) )} - - setInputValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - communicationSocket.emit(CommunicationEvents.SEND_TEXT_MESSAGE, { - roomId: getMatchId(), - message: inputValue, - username: user?.username, - createdTime: Date.now(), - }); - setInputValue(""); - } - }} - /> - - + setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && inputValue !== "") { + communicationSocket.emit(CommunicationEvents.SEND_TEXT_MESSAGE, { + roomId: getMatchId(), + message: inputValue, + username: user?.username, + createdTime: Date.now(), + }); + setInputValue(""); + } + }} + sx={{ position: "sticky", bottom: 0, zIndex: 10, background: "white" }} + /> + ); }; diff --git a/frontend/src/components/TabPanel/index.tsx b/frontend/src/components/TabPanel/index.tsx new file mode 100644 index 0000000000..cc201b83e3 --- /dev/null +++ b/frontend/src/components/TabPanel/index.tsx @@ -0,0 +1,28 @@ +import { Box, BoxProps } from "@mui/material"; + +type TabPanelProps = { + children?: React.ReactNode; + selected: string; + value: string; +}; + +const TabPanel: React.FC = ({ + children, + value, + selected, + ...others +}) => { + return ( +
+ {value === selected && {children}} +
+ ); +}; + +export default TabPanel; diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 03ad3c8b6f..44c23f7066 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -7,9 +7,11 @@ import { DialogContent, DialogContentText, DialogTitle, + Grid2, Tab, + Tabs, + Typography, } from "@mui/material"; -import { TabContext, TabList, TabPanel } from "@mui/lab"; import classes from "./index.module.css"; import { useMatch } from "../../contexts/MatchContext"; import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; @@ -22,6 +24,7 @@ import reducer, { } from "../../reducers/questionReducer"; import QuestionDetailComponent from "../../components/QuestionDetail"; import Chat from "../../components/Chat"; +import TabPanel from "../../components/TabPanel"; const CollabSandbox: React.FC = () => { const match = useMatch(); @@ -119,60 +122,56 @@ const CollabSandbox: React.FC = () => { - - - {/* Left side */} - ({ flex: 1, marginRight: theme.spacing(2) })}> + + - - {/* Right side */} - ({ - flex: 1, - marginLeft: theme.spacing(2), + + - Code editor - - - - setSelectedTab(value)}> - - - - - - Tests - - - - - + Code Editor + + setSelectedTab(value)} + sx={{ + position: "sticky", + top: 0, + zIndex: 10, + background: "white", + }} + > + + + + + Tests + + ({ + padding: theme.spacing(2), + height: "100%", + display: "flex", + flexDirection: "column", + })} + > + + - - + +
); }; From d3ee4cc89280e10b7e0f85e633dca4329d88bf45 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Tue, 29 Oct 2024 16:50:18 +0800 Subject: [PATCH 037/192] Change display --- frontend/src/components/Chat/index.tsx | 1 - frontend/src/components/TabPanel/index.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index f5f79de66c..fcba01f4a3 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -85,7 +85,6 @@ const Chat: React.FC = () => { }; }, []); - console.log(messages); return ( <> diff --git a/frontend/src/components/TabPanel/index.tsx b/frontend/src/components/TabPanel/index.tsx index cc201b83e3..8ba4038070 100644 --- a/frontend/src/components/TabPanel/index.tsx +++ b/frontend/src/components/TabPanel/index.tsx @@ -16,7 +16,7 @@ const TabPanel: React.FC = ({
From 8bc98c878233ff5520f2191b03459eda65236393 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:08:06 +0800 Subject: [PATCH 038/192] Fix port --- backend/code-execution-service/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/code-execution-service/server.ts b/backend/code-execution-service/server.ts index 27a381b398..bbcd31c8f4 100644 --- a/backend/code-execution-service/server.ts +++ b/backend/code-execution-service/server.ts @@ -1,6 +1,6 @@ import app from "./app"; -const PORT = process.env.SERVICE_PORT || 3003; +const PORT = process.env.SERVICE_PORT || 3004; if (process.env.NODE_ENV !== "test") { app.listen(PORT, () => { From a45869b4337c8d2a8eece8fb68d993817c42f94a Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 29 Oct 2024 23:42:11 +0800 Subject: [PATCH 039/192] Update FE --- frontend/src/pages/NewQuestion/index.tsx | 24 ++++++++++++++++++++++++ frontend/src/reducers/questionReducer.ts | 3 +++ 2 files changed, 27 insertions(+) diff --git a/frontend/src/pages/NewQuestion/index.tsx b/frontend/src/pages/NewQuestion/index.tsx index 29c3a894f1..fe029ed9b7 100644 --- a/frontend/src/pages/NewQuestion/index.tsx +++ b/frontend/src/pages/NewQuestion/index.tsx @@ -41,6 +41,10 @@ const NewQuestion = () => { const [uploadedImagesUrl, setUploadedImagesUrl] = useState([]); const [isPreviewQuestion, setIsPreviewQuestion] = useState(false); + const [pythonTemplate, setPythonTemplate] = useState(""); + const [javaTemplate, setJavaTemplate] = useState(""); + const [cTemplate, setCTemplate] = useState(""); + const handleBack = () => { if ( title || @@ -72,6 +76,9 @@ const NewQuestion = () => { description: markdownText, complexity: selectedComplexity, categories: selectedCategories, + pythonTemplate, + javaTemplate, + cTemplate, }, dispatch ); @@ -134,6 +141,23 @@ const NewQuestion = () => { markdownText={markdownText} setMarkdownText={setMarkdownText} /> + + {/* for the FE ppl to redesign... */} + setPythonTemplate(e.target.value)} + /> + setJavaTemplate(e.target.value)} + /> + setCTemplate(e.target.value)} + /> )} diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts index 3fa5535f8d..b7e57a7063 100644 --- a/frontend/src/reducers/questionReducer.ts +++ b/frontend/src/reducers/questionReducer.ts @@ -106,6 +106,9 @@ export const createQuestion = async ( description: question.description, complexity: question.complexity, category: question.categories, + pythonTemplate: question.pythonTemplate, + cTemplate: question.cTemplate, + javaTemplate: question.javaTemplate, }, { headers: { From 204c26bd67ef46e20a8b52f179657f00cfe423de Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 30 Oct 2024 00:49:04 +0800 Subject: [PATCH 040/192] Fix chat scroll --- frontend/src/components/Chat/index.tsx | 20 +++++++++++++++++--- frontend/src/components/TabPanel/index.tsx | 12 +++++++----- frontend/src/pages/CollabSandbox/index.tsx | 4 ++-- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index fcba01f4a3..22155f56be 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -87,7 +87,13 @@ const Chat: React.FC = () => { return ( <> - + {messages.map((msg, id) => msg.type === "bot_generated" ? ( { ) : msg.from === user?.username ? ( ({ display: "flex", justifyContent: "flex-end", @@ -133,6 +140,7 @@ const Chat: React.FC = () => { ) : ( ({ display: "flex", justifyContent: "flex-start", @@ -155,7 +163,7 @@ const Chat: React.FC = () => { { setInputValue(""); } }} - sx={{ position: "sticky", bottom: 0, zIndex: 10, background: "white" }} + sx={(theme) => ({ + position: "sticky", + bottom: 0, + zIndex: 10, + background: "white", + paddingBottom: theme.spacing(4), + })} /> ); diff --git a/frontend/src/components/TabPanel/index.tsx b/frontend/src/components/TabPanel/index.tsx index 8ba4038070..899e8135f7 100644 --- a/frontend/src/components/TabPanel/index.tsx +++ b/frontend/src/components/TabPanel/index.tsx @@ -13,15 +13,17 @@ const TabPanel: React.FC = ({ ...others }) => { return ( -
({ display: selected === value ? "flex" : "none", flexDirection: "column", - }} + padding: theme.spacing(0, 2), + })} > - {value === selected && {children}} -
+ {children} +
); }; diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 44c23f7066..d746b62244 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -139,8 +139,8 @@ const CollabSandbox: React.FC = () => { }} size={6} > - Code Editor - + Code Editor + setSelectedTab(value)} From 9d05bf994d11d4b94afdc22bc3850292a61f7160 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 30 Oct 2024 10:02:21 +0800 Subject: [PATCH 041/192] Remove newline on enter and scroll to the last message --- frontend/src/components/Chat/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 22155f56be..280bee596e 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -1,5 +1,5 @@ import { Box, TextField, Typography } from "@mui/material"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { communicationSocket } from "../../utils/communicationSocket"; import { useMatch } from "../../contexts/MatchContext"; import { @@ -35,6 +35,7 @@ const Chat: React.FC = () => { const [inputValue, setInputValue] = useState(""); const match = useMatch(); const auth = useAuth(); + const endOfMessagesRef = useRef(null); if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); @@ -85,6 +86,10 @@ const Chat: React.FC = () => { }; }, []); + useEffect(() => { + endOfMessagesRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + return ( <> { ) )} +
{ onChange={(e) => setInputValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && inputValue !== "") { + e.preventDefault(); communicationSocket.emit(CommunicationEvents.SEND_TEXT_MESSAGE, { roomId: getMatchId(), message: inputValue, From 65c9cd72f81fb57e3aca9652ff09c33261b18333 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 30 Oct 2024 10:07:09 +0800 Subject: [PATCH 042/192] Fix textbox to the bottom --- frontend/src/components/TabPanel/index.tsx | 11 +++-------- frontend/src/pages/CollabSandbox/index.tsx | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/TabPanel/index.tsx b/frontend/src/components/TabPanel/index.tsx index 899e8135f7..4dbea7c95e 100644 --- a/frontend/src/components/TabPanel/index.tsx +++ b/frontend/src/components/TabPanel/index.tsx @@ -1,4 +1,4 @@ -import { Box, BoxProps } from "@mui/material"; +import { Box } from "@mui/material"; type TabPanelProps = { children?: React.ReactNode; @@ -6,20 +6,15 @@ type TabPanelProps = { value: string; }; -const TabPanel: React.FC = ({ - children, - value, - selected, - ...others -}) => { +const TabPanel: React.FC = ({ children, value, selected }) => { return ( ({ display: selected === value ? "flex" : "none", flexDirection: "column", padding: theme.spacing(0, 2), + flex: 1, })} > {children} diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 1cae9149a3..e31a5c6107 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -169,7 +169,15 @@ const CollabSandbox: React.FC = () => { size={6} > Code Editor - + setSelectedTab(value)} @@ -186,16 +194,7 @@ const CollabSandbox: React.FC = () => { Tests - ({ - padding: theme.spacing(2), - height: "100%", - display: "flex", - flexDirection: "column", - })} - > + From 5f4834e16a529c6396d5ace3576c9d2c832fa488 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 30 Oct 2024 10:29:47 +0800 Subject: [PATCH 043/192] Add communication service into ci --- .github/workflows/ci.yml | 1 + .../communication-service/eslint.config.js | 10 + backend/communication-service/jest.config.ts | 199 + .../communication-service/package-lock.json | 6874 +++++++++++++++-- backend/communication-service/package.json | 16 +- .../tests/websocketHandler.spec.ts | 7 + backend/communication-service/tsconfig.json | 110 + docker-compose-test.yml | 14 + 8 files changed, 6460 insertions(+), 771 deletions(-) create mode 100644 backend/communication-service/eslint.config.js create mode 100644 backend/communication-service/jest.config.ts create mode 100644 backend/communication-service/tests/websocketHandler.spec.ts create mode 100644 backend/communication-service/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27ccd7ab04..c262e2a4a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: matching-service, collab-service, code-execution-service, + communication-service, ] steps: - name: Checkout code diff --git a/backend/communication-service/eslint.config.js b/backend/communication-service/eslint.config.js new file mode 100644 index 0000000000..9e38baa1fb --- /dev/null +++ b/backend/communication-service/eslint.config.js @@ -0,0 +1,10 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + { files: ["**/*.{js,mjs,cjs,ts}"] }, + { languageOptions: { globals: globals.browser } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; diff --git a/backend/communication-service/jest.config.ts b/backend/communication-service/jest.config.ts new file mode 100644 index 0000000000..151d29ec19 --- /dev/null +++ b/backend/communication-service/jest.config.ts @@ -0,0 +1,199 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +import type { Config } from "jest"; + +const config: Config = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/04/ng4c26hj1ksdsy_7x_21kvx80000gn/T/jest_dx", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + preset: "ts-jest", + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: "jest-environment-node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; + +export default config; diff --git a/backend/communication-service/package-lock.json b/backend/communication-service/package-lock.json index 5c078d2108..c3f1f68b5d 100644 --- a/backend/communication-service/package-lock.json +++ b/backend/communication-service/package-lock.json @@ -15,355 +15,659 @@ "socket.io": "^4.8.1" }, "devDependencies": { + "@eslint/js": "^9.13.0", "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", "@types/socket.io": "^3.0.1", - "tsx": "^4.19.2" + "cross-env": "^7.0.3", + "eslint": "^9.13.0", + "globals": "^15.11.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "tsx": "^4.19.2", + "typescript-eslint": "^8.12.2" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], + "node_modules/@babel/code-frame": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", + "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/compat-data": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.0.tgz", + "integrity": "sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">=18" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/core/node_modules/ms": { + "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/@babel/generator": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.0.tgz", + "integrity": "sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@babel/parser": "^7.26.0", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], + "node_modules/@babel/parser": { + "version": "7.26.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz", + "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/netbsd-x64": { + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "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/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", "cpu": [ - "x64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-arm64": { + "node_modules/@esbuild/android-arm": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/openbsd-x64": { + "node_modules/@esbuild/android-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/sunos-x64": { + "node_modules/@esbuild/android-x64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", "cpu": [ "x64" ], @@ -371,16 +675,16 @@ "license": "MIT", "optional": true, "os": [ - "sunos" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-arm64": { + "node_modules/@esbuild/darwin-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", "cpu": [ "arm64" ], @@ -388,758 +692,4636 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { + "node_modules/@esbuild/darwin-x64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { + "node_modules/@esbuild/freebsd-arm64": { "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "freebsd" ], "engines": { "node": ">=18" } }, - "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/@types/body-parser": { - "version": "1.19.5", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "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/@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "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" + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "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/@eslint/js": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", + "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.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/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, "license": "MIT", "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, + "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", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", + "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/socket.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", + "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.2.tgz", + "integrity": "sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/type-utils": "8.12.2", + "@typescript-eslint/utils": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.12.2.tgz", + "integrity": "sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "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/@typescript-eslint/scope-manager": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.12.2.tgz", + "integrity": "sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.12.2.tgz", + "integrity": "sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.12.2", + "@typescript-eslint/utils": "8.12.2", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "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/@typescript-eslint/types": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.12.2.tgz", + "integrity": "sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.12.2.tgz", + "integrity": "sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/visitor-keys": "8.12.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "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/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.12.2.tgz", + "integrity": "sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.12.2", + "@typescript-eslint/types": "8.12.2", + "@typescript-eslint/typescript-estree": "8.12.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.12.2.tgz", + "integrity": "sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.12.2", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "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/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001674", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001674.tgz", + "integrity": "sha512-jOsKlZVRnzfhLojb+Ykb+gyUSp9Xb57So+fAiFlLzzTKpqg8xxSav0e40c8/4F/v9N8QSvrRRaLeVzQbLqomYw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "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" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.49", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.49.tgz", + "integrity": "sha512-ZXfs1Of8fDb6z7WEYZjXpgIRF6MEu8JdeGA0A40aZq6OQbS+eJpnnV49epZRna2DU/YsEjSQuGtQPPtvt6J65A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", + "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.13.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/ms": { + "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/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { - "@types/node": "*" + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", - "license": "MIT" + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } }, - "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==", + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "license": "MIT", "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", - "@types/serve-static": "*" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", - "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/@types/http-errors": { + "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "22.8.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", - "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@types/qs": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", - "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" + "engines": { + "node": ">=6" } }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/@types/socket.io": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", - "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", - "dependencies": { - "socket.io": "*" + "engines": { + "node": ">=0.12.0" } }, - "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==", + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">= 0.6" + "node": ">=10" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "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", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "^4.5.0 || >= 5.9" + "node": ">=10" } }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "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/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=8" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, "engines": { - "node": ">= 0.8" + "node": ">=10" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "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==", + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">= 0.10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "detect-newline": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, "engines": { - "node": ">= 0.8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, - "funding": { - "url": "https://dotenvx.com" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/engine.io": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", - "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, "license": "MIT", "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" }, "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": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "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==", + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/engine.io/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=18" + "node": ">=6" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" }, "engines": { - "node": ">= 0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { - "node": ">= 0.8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-tsconfig": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", - "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3" + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, "engines": { - "node": ">= 0.4" + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-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" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "p-locate": "^4.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "semver": "^7.5.3" }, "engines": { - "node": ">= 0.8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, "license": "ISC" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" } }, "node_modules/media-typer": { @@ -1160,6 +5342,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -1169,6 +5368,20 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -1202,12 +5415,42 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1217,6 +5460,43 @@ "node": ">= 0.6" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1250,6 +5530,137 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1259,12 +5670,144 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "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==", "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1278,6 +5821,33 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -1293,6 +5863,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1317,6 +5908,64 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1327,6 +5976,51 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1353,6 +6047,16 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -1430,6 +6134,29 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -1448,6 +6175,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -1493,87 +6244,390 @@ } } }, - "node_modules/socket.io-adapter/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "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/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, "license": "MIT" }, - "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==", + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "is-number": "^7.0.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=8.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==", + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=0.6" } }, - "node_modules/socket.io-parser/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } }, - "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==", + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" }, "engines": { - "node": ">=6.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { - "supports-color": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { "optional": true } } }, - "node_modules/socket.io/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">= 0.8" + "node": ">=10" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=0.6" + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } } }, "node_modules/tsx": { @@ -1596,6 +6650,42 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1609,6 +6699,45 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.12.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.12.2.tgz", + "integrity": "sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.12.2", + "@typescript-eslint/parser": "8.12.2", + "@typescript-eslint/utils": "8.12.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -1624,6 +6753,47 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1633,6 +6803,28 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1642,6 +6834,81 @@ "node": ">= 0.8" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", @@ -1662,6 +6929,75 @@ "optional": true } } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/backend/communication-service/package.json b/backend/communication-service/package.json index 9d89c84480..d36d8fbe3b 100644 --- a/backend/communication-service/package.json +++ b/backend/communication-service/package.json @@ -2,10 +2,13 @@ "name": "communication-service", "version": "1.0.0", "main": "server.ts", + "type": "module", "scripts": { "start": "tsx src/server.ts", "dev": "tsx watch src/server.ts", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "cross-env NODE_ENV=test && jest", + "test:watch": "cross-env NODE_ENV=test && jest --watch", + "lint": "eslint ." }, "author": "", "license": "ISC", @@ -17,8 +20,17 @@ "socket.io": "^4.8.1" }, "devDependencies": { + "@eslint/js": "^9.13.0", "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", "@types/socket.io": "^3.0.1", - "tsx": "^4.19.2" + "cross-env": "^7.0.3", + "eslint": "^9.13.0", + "globals": "^15.11.0", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "tsx": "^4.19.2", + "typescript-eslint": "^8.12.2" } } diff --git a/backend/communication-service/tests/websocketHandler.spec.ts b/backend/communication-service/tests/websocketHandler.spec.ts new file mode 100644 index 0000000000..0e709d5c75 --- /dev/null +++ b/backend/communication-service/tests/websocketHandler.spec.ts @@ -0,0 +1,7 @@ +// todo: add test cases + +describe("Test web socket", () => { + it("Test", () => { + expect(true); + }); +}); diff --git a/backend/communication-service/tsconfig.json b/backend/communication-service/tsconfig.json new file mode 100644 index 0000000000..34059b779a --- /dev/null +++ b/backend/communication-service/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "noEmit": true /* Disable emitting files from a compilation. */, + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 7e5ea5e7df..ddab723cfa 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -98,6 +98,20 @@ services: restart: on-failure command: ["npm", "test"] + test-communication-service: + image: peerprep/communication-service + build: ./backend/communication-service + environment: + - NODE_ENV=test + - SERVICE_PORT=3005 + networks: + - peerprep-network + volumes: + - ./backend/communication-service:/communication-service + - /communication-service/node_modules + restart: on-failure + command: ["npm", "test"] + test-frontend: image: peerprep/frontend build: ./frontend From 33554eca28abe70af8e7b69047e536963666a456 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 30 Oct 2024 11:10:14 +0800 Subject: [PATCH 044/192] Add tests --- .../communication-service/package-lock.json | 98 +++++++++++++++++++ backend/communication-service/package.json | 2 + .../tests/websocketHandler.spec.ts | 93 +++++++++++++++++- 3 files changed, 189 insertions(+), 4 deletions(-) diff --git a/backend/communication-service/package-lock.json b/backend/communication-service/package-lock.json index c3f1f68b5d..20d788705e 100644 --- a/backend/communication-service/package-lock.json +++ b/backend/communication-service/package-lock.json @@ -19,10 +19,12 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/socket.io": "^3.0.1", + "@types/socket.io-client": "^1.4.36", "cross-env": "^7.0.3", "eslint": "^9.13.0", "globals": "^15.11.0", "jest": "^29.7.0", + "socket.io-client": "^4.8.1", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsx": "^4.19.2", @@ -1990,6 +1992,13 @@ "socket.io": "*" } }, + "node_modules/@types/socket.io-client": { + "version": "1.4.36", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", + "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3222,6 +3231,45 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", + "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "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/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -6250,6 +6298,47 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "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/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -6930,6 +7019,15 @@ } } }, + "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==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/communication-service/package.json b/backend/communication-service/package.json index d36d8fbe3b..c7528cb505 100644 --- a/backend/communication-service/package.json +++ b/backend/communication-service/package.json @@ -24,10 +24,12 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/socket.io": "^3.0.1", + "@types/socket.io-client": "^1.4.36", "cross-env": "^7.0.3", "eslint": "^9.13.0", "globals": "^15.11.0", "jest": "^29.7.0", + "socket.io-client": "^4.8.1", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsx": "^4.19.2", diff --git a/backend/communication-service/tests/websocketHandler.spec.ts b/backend/communication-service/tests/websocketHandler.spec.ts index 0e709d5c75..387d195128 100644 --- a/backend/communication-service/tests/websocketHandler.spec.ts +++ b/backend/communication-service/tests/websocketHandler.spec.ts @@ -1,7 +1,92 @@ -// todo: add test cases +import { createServer } from "node:http"; +import { type AddressInfo } from "node:net"; +import ioc from "socket.io-client"; +import { Server, Socket } from "socket.io"; -describe("Test web socket", () => { - it("Test", () => { - expect(true); +import { BOT_NAME } from "../src/utils/constants"; +import { CommunicationEvents, MessageTypes } from "../src/utils/types"; + +describe("Communication service web socket", () => { + let io: Server, serverSocket: Socket, clientSocket: SocketIOClient.Socket; + + beforeAll((done) => { + const httpServer = createServer(); + io = new Server(httpServer); + httpServer.listen(() => { + const port = (httpServer.address() as AddressInfo).port; + clientSocket = ioc(`http://localhost:${port}`); + io.on("connection", (socket) => { + serverSocket = socket; + }); + clientSocket.on("connect", done); + }); + }); + + afterAll(() => { + io.close(); + clientSocket.close(); + }); + + it("User joined acknowledgement", (done) => { + const createdAt = Date.now(); + const messageSent = "User has joined the chat"; + clientSocket.on( + CommunicationEvents.USER_JOINED, + ({ + from, + type, + message, + createdTime, + }: { + from: string; + type: string; + message: string; + createdTime: number; + }) => { + expect(from).toBe(BOT_NAME); + expect(type).toBe(MessageTypes.BOT_GENERATED); + expect(message).toBe(messageSent); + expect(createdTime).toBe(createdAt); + done(); + } + ); + serverSocket.emit(CommunicationEvents.USER_JOINED, { + from: BOT_NAME, + type: MessageTypes.BOT_GENERATED, + message: messageSent, + createdTime: createdAt, + }); + }); + + it("Message received", (done) => { + const createdAt = Date.now(); + const messageSent = "Hello"; + const username = "user"; + clientSocket.on( + CommunicationEvents.TEXT_MESSAGE_RECEIVED, + ({ + from, + type, + message, + createdTime, + }: { + from: string; + type: string; + message: string; + createdTime: number; + }) => { + expect(from).toBe(username); + expect(type).toBe(MessageTypes.USER_GENERATED); + expect(message).toBe(messageSent); + expect(createdTime).toBe(createdAt); + done(); + } + ); + serverSocket.emit(CommunicationEvents.TEXT_MESSAGE_RECEIVED, { + from: username, + type: MessageTypes.USER_GENERATED, + message: messageSent, + createdTime: createdAt, + }); }); }); From 81b43be0c7ff2d5a484f3bfb9d72739a9dd9d946 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 30 Oct 2024 17:39:50 +0800 Subject: [PATCH 045/192] Fix scrolling and trim input --- frontend/src/components/Chat/index.tsx | 26 ++++++++++++++++------ frontend/src/pages/CollabSandbox/index.tsx | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 280bee596e..710d2095d3 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -30,12 +30,16 @@ enum CommunicationEvents { DISCONNECTED = "disconnected", } -const Chat: React.FC = () => { +type ChatProps = { + isActive: boolean; +}; + +const Chat: React.FC = ({ isActive }) => { const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(""); const match = useMatch(); const auth = useAuth(); - const endOfMessagesRef = useRef(null); + const messagesRef = useRef(null); if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); @@ -87,8 +91,14 @@ const Chat: React.FC = () => { }, []); useEffect(() => { - endOfMessagesRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); + if (messagesRef.current) { + const nodes = messagesRef.current.querySelectorAll("div > div"); + if (nodes.length > 0) { + const lastNode = nodes[nodes.length - 1]; + lastNode.scrollIntoView({ behavior: "smooth" }); + } + } + }, [messages, isActive]); return ( <> @@ -98,6 +108,7 @@ const Chat: React.FC = () => { overflowY: "auto", padding: 2, }} + ref={messagesRef} > {messages.map((msg, id) => msg.type === "bot_generated" ? ( @@ -166,7 +177,7 @@ const Chat: React.FC = () => { ) )} -
+ {/*
*/} { value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={(e) => { - if (e.key === "Enter" && inputValue !== "") { + const trimmedValue = inputValue.trim(); + if (e.key === "Enter" && trimmedValue !== "") { e.preventDefault(); communicationSocket.emit(CommunicationEvents.SEND_TEXT_MESSAGE, { roomId: getMatchId(), - message: inputValue, + message: trimmedValue, username: user?.username, createdTime: Date.now(), }); diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index e31a5c6107..b621bf1829 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -195,7 +195,7 @@ const CollabSandbox: React.FC = () => { Tests - + From 21c4d2ae7b7b54b8e980a34f16ff196facb765e6 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 30 Oct 2024 17:40:19 +0800 Subject: [PATCH 046/192] Remove unused code --- frontend/src/components/Chat/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 710d2095d3..bb0f3d26ca 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -177,7 +177,6 @@ const Chat: React.FC = ({ isActive }) => { ) )} - {/*
*/} Date: Wed, 30 Oct 2024 17:50:37 +0800 Subject: [PATCH 047/192] Add border below tabs --- frontend/src/pages/CollabSandbox/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index b621bf1829..713138894d 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -181,12 +181,13 @@ const CollabSandbox: React.FC = () => { setSelectedTab(value)} - sx={{ + sx={(theme) => ({ position: "sticky", top: 0, zIndex: 10, background: "white", - }} + borderBottom: `1px solid ${theme.palette.divider}`, + })} > From bb8c950fc8c126f88399f05dd7035a17d81e2ad6 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 30 Oct 2024 18:23:04 +0800 Subject: [PATCH 048/192] Add test case components --- frontend/src/components/TestCase/index.tsx | 49 +++++++++++++++++++++ frontend/src/pages/CollabSandbox/index.tsx | 51 +++++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/TestCase/index.tsx diff --git a/frontend/src/components/TestCase/index.tsx b/frontend/src/components/TestCase/index.tsx new file mode 100644 index 0000000000..424eee0004 --- /dev/null +++ b/frontend/src/components/TestCase/index.tsx @@ -0,0 +1,49 @@ +import { Box, styled, Typography } from "@mui/material"; + +type TestCaseProps = { + input: string; + output: string; + stdout: string; + result: string; +}; + +const StyledBox = styled(Box)(({ theme }) => ({ + margin: theme.spacing(2, 0), +})); + +const StyledTypography = styled(Typography)(({ theme }) => ({ + background: theme.palette.divider, + padding: theme.spacing(1, 2), + borderRadius: theme.spacing(1), + whiteSpace: "pre-line", +})); + +const TestCase: React.FC = ({ + input, + output, + stdout, + result, +}) => { + return ( + + ({ marginBottom: theme.spacing(2) })}> + Input + {input} + + + Output + {output} + + + Standard output + {stdout} + + + Result + {result} + + + ); +}; + +export default TestCase; diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 713138894d..9d2bf9afb4 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -10,7 +10,6 @@ import { Grid2, Tab, Tabs, - Typography, } from "@mui/material"; import classes from "./index.module.css"; import { useMatch } from "../../contexts/MatchContext"; @@ -26,6 +25,31 @@ import QuestionDetailComponent from "../../components/QuestionDetail"; import { Navigate } from "react-router-dom"; import Chat from "../../components/Chat"; import TabPanel from "../../components/TabPanel"; +import TestCase from "../../components/TestCase"; + +// hardcode for now... + +type TestCase = { + input: string; + output: string; + stdout: string; + result: string; +}; + +const testcases: TestCase[] = [ + { + input: "1 2 3 4", + output: "1 2 3 4", + stdout: "1\n2\n3\n4", + result: "1 2 3 4", + }, + { + input: "5 6 7 8", + output: "5 6 7 8", + stdout: "5\n6\n7\n8", + result: "5 6 7 8", + }, +]; const CollabSandbox: React.FC = () => { const [showErrorScreen, setShowErrorScreen] = useState(false); @@ -48,6 +72,7 @@ const CollabSandbox: React.FC = () => { const [state, dispatch] = useReducer(reducer, initialState); const { selectedQuestion } = state; const [selectedTab, setSelectedTab] = useState<"tests" | "chat">("tests"); + const [selectedTestcase, setSelectedTestcase] = useState(0); useEffect(() => { if (!partner) { @@ -193,7 +218,29 @@ const CollabSandbox: React.FC = () => { - Tests + ({ margin: theme.spacing(2, 0) })}> + {[...Array(testcases.length)] + .map((_, index) => index + 1) + .map((i) => ( + + ))} + + From 3a02277bfb63ea7b2022bd538de0e5410e410c93 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Wed, 30 Oct 2024 21:58:34 +0800 Subject: [PATCH 049/192] Add test case input fields --- frontend/package-lock.json | 14 +++ frontend/package.json | 1 + .../components/QuestionTestCases/index.tsx | 118 ++++++++++++++++++ frontend/src/pages/NewQuestion/index.tsx | 59 ++++++--- 4 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 frontend/src/components/QuestionTestCases/index.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5833bd7fd3..7cc1928eed 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "react-router-dom": "^6.3.0", "react-toastify": "^10.0.5", "socket.io-client": "^4.8.0", + "uuid": "^11.0.2", "vite-plugin-svgr": "^4.2.0" }, "devDependencies": { @@ -12839,6 +12840,19 @@ "requires-port": "^1.0.0" } }, + "node_modules/uuid": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", + "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7f0aba9259..bd7ec55a8b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "react-router-dom": "^6.3.0", "react-toastify": "^10.0.5", "socket.io-client": "^4.8.0", + "uuid": "^11.0.2", "vite-plugin-svgr": "^4.2.0" }, "devDependencies": { diff --git a/frontend/src/components/QuestionTestCases/index.tsx b/frontend/src/components/QuestionTestCases/index.tsx new file mode 100644 index 0000000000..a8ac6687b6 --- /dev/null +++ b/frontend/src/components/QuestionTestCases/index.tsx @@ -0,0 +1,118 @@ +import { Box, Button, Stack, TextField, Typography } from "@mui/material"; +import { Dispatch, SetStateAction } from "react"; +import { v4 as uuidv4 } from "uuid"; + +interface QuestionTestCasesProps { + testCases: TestCase[]; + setTestCases: Dispatch>; +} + +export interface TestCase { + id: string; + input: string; + expectedOutput: string; +} + +const QuestionTestCases: React.FC = ({ + testCases, + setTestCases, +}) => { + const handleAddTestCase = () => { + if (testCases.length < 3) { + setTestCases((testCases) => [ + ...testCases, + { id: uuidv4(), input: "", expectedOutput: "" }, + ]); + } + }; + + const handleDeleteTestCase = (testCaseId: string) => { + setTestCases((testCases) => + testCases.filter((testCase) => testCase.id !== testCaseId) + ); + }; + + const handleInputChange = ( + testCaseId: string, + field: keyof TestCase, + value: string + ) => { + setTestCases((testCases) => + testCases.map((testCase) => + testCase.id === testCaseId ? { ...testCase, [field]: value } : testCase + ) + ); + }; + + return ( + + {testCases.map((testCase, i) => ( + + + Test Case {i + 1} + + {i === testCases.length - 1 && testCases.length < 3 ? ( + <> + {i === 0 ? ( + <> + ) : ( + + )} + + + ) : ( + + )} + + + + handleInputChange(testCase.id, "input", e.target.value) + } + fullWidth + margin="normal" + /> + + handleInputChange(testCase.id, "expectedOutput", e.target.value) + } + fullWidth + margin="normal" + /> + + ))} + + ); +}; + +export default QuestionTestCases; diff --git a/frontend/src/pages/NewQuestion/index.tsx b/frontend/src/pages/NewQuestion/index.tsx index fe029ed9b7..f56b1847f9 100644 --- a/frontend/src/pages/NewQuestion/index.tsx +++ b/frontend/src/pages/NewQuestion/index.tsx @@ -26,6 +26,10 @@ import QuestionMarkdown from "../../components/QuestionMarkdown"; import QuestionImageContainer from "../../components/QuestionImageContainer"; import QuestionCategoryAutoComplete from "../../components/QuestionCategoryAutoComplete"; import QuestionDetail from "../../components/QuestionDetail"; +import QuestionTestCases, { + TestCase, +} from "../../components/QuestionTestCases"; +import { v4 as uuidv4 } from "uuid"; const NewQuestion = () => { const navigate = useNavigate(); @@ -41,6 +45,10 @@ const NewQuestion = () => { const [uploadedImagesUrl, setUploadedImagesUrl] = useState([]); const [isPreviewQuestion, setIsPreviewQuestion] = useState(false); + const [testCases, setTestCases] = useState([ + { id: uuidv4(), input: "", expectedOutput: "" }, + ]); + const [pythonTemplate, setPythonTemplate] = useState(""); const [javaTemplate, setJavaTemplate] = useState(""); const [cTemplate, setCTemplate] = useState(""); @@ -64,31 +72,37 @@ const NewQuestion = () => { !title || !markdownText || !selectedComplexity || - selectedCategories.length === 0 + selectedCategories.length === 0 || + testCases.some( + (testCase) => + testCase.input.trim() === "" || testCase.expectedOutput.trim() === "" + ) ) { toast.error(FILL_ALL_FIELDS); return; } - const result = await createQuestion( - { - title, - description: markdownText, - complexity: selectedComplexity, - categories: selectedCategories, - pythonTemplate, - javaTemplate, - cTemplate, - }, - dispatch - ); - - if (result) { - navigate("/questions"); - toast.success(SUCCESS_QUESTION_CREATE); - } else { - toast.error(state.selectedQuestionError || FAILED_QUESTION_CREATE); - } + // const result = await createQuestion( + // { + // title, + // description: markdownText, + // complexity: selectedComplexity, + // categories: selectedCategories, + // pythonTemplate, + // javaTemplate, + // cTemplate, + // }, + // dispatch + // ); + + // if (result) { + // navigate("/questions"); + // toast.success(SUCCESS_QUESTION_CREATE); + // } else { + // toast.error(state.selectedQuestionError || FAILED_QUESTION_CREATE); + // } + + console.log("successfully submit") }; return ( @@ -142,6 +156,11 @@ const NewQuestion = () => { setMarkdownText={setMarkdownText} /> + + {/* for the FE ppl to redesign... */} Date: Wed, 30 Oct 2024 21:58:58 +0800 Subject: [PATCH 050/192] Allow users to enter newline using shift+enter --- frontend/src/components/Chat/index.tsx | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index bb0f3d26ca..8c5da246c3 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -1,4 +1,4 @@ -import { Box, TextField, Typography } from "@mui/material"; +import { Box, styled, TextField, Typography } from "@mui/material"; import { useEffect, useRef, useState } from "react"; import { communicationSocket } from "../../utils/communicationSocket"; import { useMatch } from "../../contexts/MatchContext"; @@ -34,6 +34,13 @@ type ChatProps = { isActive: boolean; }; +const StyledTypography = styled(Typography)(({ theme }) => ({ + padding: theme.spacing(1, 2), + borderRadius: theme.spacing(2), + maxWidth: "80%", + whiteSpace: "pre-line", +})); + const Chat: React.FC = ({ isActive }) => { const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(""); @@ -143,16 +150,13 @@ const Chat: React.FC = ({ isActive }) => { marginTop: theme.spacing(1), })} > - ({ background: theme.palette.primary.main, - padding: theme.spacing(1, 2), - borderRadius: theme.spacing(2), - maxWidth: "80%", })} > {msg.message} - + ) : ( = ({ isActive }) => { marginTop: theme.spacing(1), })} > - ({ background: theme.palette.secondary.main, - padding: theme.spacing(1, 2), - borderRadius: theme.spacing(2), - maxWidth: "80%", })} > {msg.message} - + ) )} @@ -186,7 +187,7 @@ const Chat: React.FC = ({ isActive }) => { onChange={(e) => setInputValue(e.target.value)} onKeyDown={(e) => { const trimmedValue = inputValue.trim(); - if (e.key === "Enter" && trimmedValue !== "") { + if (e.key === "Enter" && !e.shiftKey && trimmedValue !== "") { e.preventDefault(); communicationSocket.emit(CommunicationEvents.SEND_TEXT_MESSAGE, { roomId: getMatchId(), From 71695dc81a4e5126c68fde809ff75359d89e0e4d Mon Sep 17 00:00:00 2001 From: jolynloh Date: Wed, 30 Oct 2024 22:25:02 +0800 Subject: [PATCH 051/192] Add tooltip help message for test cases --- .../components/QuestionTestCases/index.tsx | 37 ++++++++++++++++++- frontend/src/utils/constants.ts | 3 ++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/QuestionTestCases/index.tsx b/frontend/src/components/QuestionTestCases/index.tsx index a8ac6687b6..ecc429331c 100644 --- a/frontend/src/components/QuestionTestCases/index.tsx +++ b/frontend/src/components/QuestionTestCases/index.tsx @@ -1,6 +1,16 @@ -import { Box, Button, Stack, TextField, Typography } from "@mui/material"; +import { HelpOutlined } from "@mui/icons-material"; +import { + Box, + Button, + IconButton, + Stack, + TextField, + Tooltip, + Typography, +} from "@mui/material"; import { Dispatch, SetStateAction } from "react"; import { v4 as uuidv4 } from "uuid"; +import { ADD_QUESTION_TEST_CASE_TOOLTIP_MESSAGE } from "../../utils/constants"; interface QuestionTestCasesProps { testCases: TestCase[]; @@ -58,7 +68,30 @@ const QuestionTestCases: React.FC = ({ alignItems="center" justifyContent="space-between" > - Test Case {i + 1} + + Test Case {i + 1} + {i === 0 ? ( + + + + } + placement="right" + arrow + > + + + + + ) : ( + <> + )} + {i === testCases.length - 1 && testCases.length < 3 ? ( <> diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index dad0b7a81e..05da092d35 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -113,3 +113,6 @@ export const FIND_MATCH_FORM_PATH = "/find_match_form.png"; export const MATCH_FOUND_PATH = "/match_found.png"; export const QUESTIONS_LIST_PATH = "/questions_list.png"; export const COLLABORATIVE_EDITOR_PATH = "/collaborative_editor.png"; + +/* Tooltips */ +export const ADD_QUESTION_TEST_CASE_TOOLTIP_MESSAGE = `Add at least 1 and at most 3 test cases.
This will be displayed to users.`; From 467baae59431cc127fff9beacca61b62fd7935e4 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Thu, 31 Oct 2024 01:23:04 +0800 Subject: [PATCH 052/192] Add file upload component --- .../QuestionFileContainer/index.tsx | 51 +++++++++++++++++++ .../QuestionImageContainer/index.tsx | 2 +- frontend/src/pages/NewQuestion/index.tsx | 16 +++++- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/QuestionFileContainer/index.tsx diff --git a/frontend/src/components/QuestionFileContainer/index.tsx b/frontend/src/components/QuestionFileContainer/index.tsx new file mode 100644 index 0000000000..5fa2813260 --- /dev/null +++ b/frontend/src/components/QuestionFileContainer/index.tsx @@ -0,0 +1,51 @@ +import { UploadFileOutlined } from "@mui/icons-material"; +import { Button, styled } from "@mui/material"; + +interface QuestionFileContainerProps { + fileUploadMessage: string; +} + +const FileUploadInput = styled("input")({ + height: 1, + overflow: "hidden", + position: "absolute", + bottom: 0, + left: 0, + width: 1, +}); + +const QuestionFileContainer: React.FC = ({ + fileUploadMessage, +}) => { + const handleFileUpload = (event: React.ChangeEvent) => {}; + + return ( + + ); +}; + +export default QuestionFileContainer; diff --git a/frontend/src/components/QuestionImageContainer/index.tsx b/frontend/src/components/QuestionImageContainer/index.tsx index 1b48bb5e13..9bbc5c38d3 100644 --- a/frontend/src/components/QuestionImageContainer/index.tsx +++ b/frontend/src/components/QuestionImageContainer/index.tsx @@ -41,7 +41,7 @@ const QuestionImageContainer: React.FC = ({ }; const handleImageUpload = async ( - event: React.ChangeEvent, + event: React.ChangeEvent ) => { if (!event.target.files) { return; diff --git a/frontend/src/pages/NewQuestion/index.tsx b/frontend/src/pages/NewQuestion/index.tsx index f56b1847f9..dee05f9dd5 100644 --- a/frontend/src/pages/NewQuestion/index.tsx +++ b/frontend/src/pages/NewQuestion/index.tsx @@ -30,6 +30,7 @@ import QuestionTestCases, { TestCase, } from "../../components/QuestionTestCases"; import { v4 as uuidv4 } from "uuid"; +import QuestionFileContainer from "../../components/QuestionFileContainer"; const NewQuestion = () => { const navigate = useNavigate(); @@ -102,7 +103,7 @@ const NewQuestion = () => { // toast.error(state.selectedQuestionError || FAILED_QUESTION_CREATE); // } - console.log("successfully submit") + console.log("successfully submit"); }; return ( @@ -161,6 +162,19 @@ const NewQuestion = () => { setTestCases={setTestCases} /> + + + + + {/* for the FE ppl to redesign... */} Date: Thu, 31 Oct 2024 12:44:33 +0800 Subject: [PATCH 053/192] Complete file upload FE implementation --- .../QuestionFileContainer/index.tsx | 56 +++++++++++++++-- .../components/QuestionTestCases/index.tsx | 2 +- .../QuestionTestCasesFileUpload/index.tsx | 62 +++++++++++++++++++ frontend/src/pages/NewQuestion/index.tsx | 28 ++++----- frontend/src/utils/constants.ts | 1 + 5 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 frontend/src/components/QuestionTestCasesFileUpload/index.tsx diff --git a/frontend/src/components/QuestionFileContainer/index.tsx b/frontend/src/components/QuestionFileContainer/index.tsx index 5fa2813260..ed53647adb 100644 --- a/frontend/src/components/QuestionFileContainer/index.tsx +++ b/frontend/src/components/QuestionFileContainer/index.tsx @@ -1,8 +1,14 @@ -import { UploadFileOutlined } from "@mui/icons-material"; +import FileUploadIcon from "@mui/icons-material/FileUpload"; import { Button, styled } from "@mui/material"; +import { useState } from "react"; +import { toast } from "react-toastify"; interface QuestionFileContainerProps { fileUploadMessage: string; + marginLeft?: number; + marginRight?: number; + file: File | null; + setFile: React.Dispatch>; } const FileUploadInput = styled("input")({ @@ -16,8 +22,47 @@ const FileUploadInput = styled("input")({ const QuestionFileContainer: React.FC = ({ fileUploadMessage, + marginLeft, + marginRight, + file, + setFile, }) => { - const handleFileUpload = (event: React.ChangeEvent) => {}; + const handleFileUpload = (event: React.ChangeEvent) => { + if (!event.target.files) { + return; + } + + const file = event.target.files[0]; + if (!file.type.startsWith("text/")) { + toast.error(`${file.name} is not a text file`); + return; + } + + if (!file.size) { + toast.error(`${file.name} is empty. Please upload another text file.`); + return; + } + + setFile(file); + console.log(event); + + // const formData = new FormData(); + + // if (formData.getAll("images[]").length === 0) { + // return; + // } + + // createImageUrls(formData).then((res) => { + // if (res) { + // for (const imageUrl of res.imageUrls) { + // setUploadedImagesUrl((prev) => [...prev, imageUrl]); + // } + // toast.success(SUCCESS_FILE_UPLOAD); + // } else { + // toast.error(FAILED_FILE_UPLOAD); + // } + // }); + }; return ( ); diff --git a/frontend/src/components/QuestionTestCases/index.tsx b/frontend/src/components/QuestionTestCases/index.tsx index ecc429331c..65d444d40e 100644 --- a/frontend/src/components/QuestionTestCases/index.tsx +++ b/frontend/src/components/QuestionTestCases/index.tsx @@ -55,7 +55,7 @@ const QuestionTestCases: React.FC = ({ }; return ( - + {testCases.map((testCase, i) => ( >; + testcaseOutputFile: File | null; + setTestcaseOutputFile: React.Dispatch>; +} + +const QuestionTestCasesFileUpload: React.FC< + QuestionTestCasesFileUploadProps +> = ({ + testcaseInputFile, + setTestcaseInputFile, + testcaseOutputFile, + setTestcaseOutputFile, +}) => { + return ( + + + Test Cases File Upload + + + + } + placement="right" + arrow + > + + + + + + + + + + + + ); +}; + +export default QuestionTestCasesFileUpload; diff --git a/frontend/src/pages/NewQuestion/index.tsx b/frontend/src/pages/NewQuestion/index.tsx index dee05f9dd5..7b68524c20 100644 --- a/frontend/src/pages/NewQuestion/index.tsx +++ b/frontend/src/pages/NewQuestion/index.tsx @@ -30,7 +30,7 @@ import QuestionTestCases, { TestCase, } from "../../components/QuestionTestCases"; import { v4 as uuidv4 } from "uuid"; -import QuestionFileContainer from "../../components/QuestionFileContainer"; +import QuestionTestCasesFileUpload from "../../components/QuestionTestCasesFileUpload"; const NewQuestion = () => { const navigate = useNavigate(); @@ -49,6 +49,10 @@ const NewQuestion = () => { const [testCases, setTestCases] = useState([ { id: uuidv4(), input: "", expectedOutput: "" }, ]); + const [testcaseInputFile, setTestcaseInputFile] = useState(null); + const [testcaseOutputFile, setTestcaseOutputFile] = useState( + null + ); const [pythonTemplate, setPythonTemplate] = useState(""); const [javaTemplate, setJavaTemplate] = useState(""); @@ -77,7 +81,9 @@ const NewQuestion = () => { testCases.some( (testCase) => testCase.input.trim() === "" || testCase.expectedOutput.trim() === "" - ) + ) || + testcaseInputFile === null || + testcaseOutputFile === null ) { toast.error(FILL_ALL_FIELDS); return; @@ -162,18 +168,12 @@ const NewQuestion = () => { setTestCases={setTestCases} /> - - - - + {/* for the FE ppl to redesign... */} This will be displayed to users.`; +export const ADD_TEST_CASE_FILES_TOOLTIP_MESSAGE = `Upload files for executing test cases when user submits code.

This is a required field. Only text files accepted.`; From c19981bcb44f32849072b43ac48ee725f664516c Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:59:14 +0800 Subject: [PATCH 054/192] Add qn history microservice -create qnhistory record on match accepted -add qnhistory table to profile --- .../src/handlers/websocketHandler.ts | 50 +- backend/matching-service/src/utils/api.ts | 11 + backend/qn-history-service/.dockerignore | 5 + backend/qn-history-service/.env.sample | 23 + backend/qn-history-service/Dockerfile | 13 + backend/qn-history-service/README.md | 29 + backend/qn-history-service/app.ts | 37 + backend/qn-history-service/config/db.ts | 24 + backend/qn-history-service/eslint.config.js | 22 + backend/qn-history-service/jest.config.ts | 199 + backend/qn-history-service/package-lock.json | 6692 +++++++++++++++++ backend/qn-history-service/package.json | 41 + backend/qn-history-service/server.ts | 21 + .../controllers/questionHistoryController.ts | 195 + .../src/models/QnHistory.ts | 34 + .../src/routes/questionHistoryRoutes.ts | 22 + .../qn-history-service/src/utils/constants.ts | 21 + backend/qn-history-service/swagger.yml | 306 + backend/qn-history-service/tsconfig.json | 110 + docker-compose.yml | 44 + frontend/src/contexts/MatchContext.tsx | 21 +- frontend/src/pages/Profile/index.tsx | 116 +- frontend/src/reducers/qnHistoryReducer.ts | 317 + frontend/src/utils/api.ts | 6 + frontend/src/utils/sessionTime.ts | 10 + 25 files changed, 8347 insertions(+), 22 deletions(-) create mode 100644 backend/qn-history-service/.dockerignore create mode 100644 backend/qn-history-service/.env.sample create mode 100644 backend/qn-history-service/Dockerfile create mode 100644 backend/qn-history-service/README.md create mode 100644 backend/qn-history-service/app.ts create mode 100644 backend/qn-history-service/config/db.ts create mode 100644 backend/qn-history-service/eslint.config.js create mode 100644 backend/qn-history-service/jest.config.ts create mode 100644 backend/qn-history-service/package-lock.json create mode 100644 backend/qn-history-service/package.json create mode 100644 backend/qn-history-service/server.ts create mode 100644 backend/qn-history-service/src/controllers/questionHistoryController.ts create mode 100644 backend/qn-history-service/src/models/QnHistory.ts create mode 100644 backend/qn-history-service/src/routes/questionHistoryRoutes.ts create mode 100644 backend/qn-history-service/src/utils/constants.ts create mode 100644 backend/qn-history-service/swagger.yml create mode 100644 backend/qn-history-service/tsconfig.json create mode 100644 frontend/src/reducers/qnHistoryReducer.ts diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index 52c31f354a..3c71cfb2c2 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -11,7 +11,7 @@ import { } from "./matchHandler"; import { io } from "../../server"; import { v4 as uuidv4 } from "uuid"; -import { questionService } from "../utils/api"; +import { qnHistoryService, questionService } from "../utils/api"; enum MatchEvents { // Receive @@ -120,23 +120,41 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { userConnections.delete(uid); }); - socket.on(MatchEvents.MATCH_ACCEPT_REQUEST, (matchId: string) => { - const partnerAccepted = handleMatchAccept(matchId); - if (partnerAccepted) { - const match = getMatchById(matchId); - if (!match) { - return; - } + socket.on( + MatchEvents.MATCH_ACCEPT_REQUEST, + (matchId: string, userId1: string, userId2: string) => { + const partnerAccepted = handleMatchAccept(matchId); + if (partnerAccepted) { + const match = getMatchById(matchId); + if (!match) { + return; + } - const { complexity, category } = match; - questionService - .get("/random", { params: { complexity, category } }) - .then((res) => { - const { id } = res.data.question; - io.to(matchId).emit(MatchEvents.MATCH_SUCCESSFUL, id); - }); + const { complexity, category } = match; + questionService + .get("/random", { params: { complexity, category } }) + .then((res) => { + const qnId = res.data.question.id; + qnHistoryService + .post("/", { + userIds: [userId1, userId2], + questionId: qnId, + title: res.data.question.title, + submissionStatus: "Attempted", + dateAttempted: new Date(), + timeTaken: 0, + }) + .then((res) => { + io.to(matchId).emit( + MatchEvents.MATCH_SUCCESSFUL, + qnId, + res.data.qnHistory.id + ); + }); + }); + } } - }); + ); socket.on( MatchEvents.MATCH_DECLINE_REQUEST, diff --git a/backend/matching-service/src/utils/api.ts b/backend/matching-service/src/utils/api.ts index 76c414befc..36e5dfd617 100644 --- a/backend/matching-service/src/utils/api.ts +++ b/backend/matching-service/src/utils/api.ts @@ -4,9 +4,20 @@ const QUESTION_SERVICE_URL = process.env.QUESTION_SERVICE_URL || "http://question-service:3000/api/questions"; +const QN_HISTORY_SERVICE_URL = + process.env.QN_HISTORY_SERVICE_URL || + "http://qn-history-service:3005/api/qnhistories"; + export const questionService = axios.create({ baseURL: QUESTION_SERVICE_URL, headers: { "Content-Type": "application/json", }, }); + +export const qnHistoryService = axios.create({ + baseURL: QN_HISTORY_SERVICE_URL, + headers: { + "Content-Type": "application/json", + }, +}); diff --git a/backend/qn-history-service/.dockerignore b/backend/qn-history-service/.dockerignore new file mode 100644 index 0000000000..4abc77f632 --- /dev/null +++ b/backend/qn-history-service/.dockerignore @@ -0,0 +1,5 @@ +coverage +node_modules +tests +.env* +*.md diff --git a/backend/qn-history-service/.env.sample b/backend/qn-history-service/.env.sample new file mode 100644 index 0000000000..7c9f085eac --- /dev/null +++ b/backend/qn-history-service/.env.sample @@ -0,0 +1,23 @@ +NODE_ENV=development +SERVICE_PORT=3005 + +ORIGINS=http://localhost:5173,http://127.0.0.1:5173 + +# if using cloud MongoDB, replace with actual URI (run service separately) +MONGO_CLOUD_URI= + +# if using local MongoDB (run service with docker-compose) +## MongoDB credentials +MONGO_INITDB_ROOT_USERNAME=root +MONGO_INITDB_ROOT_PASSWORD=example + +## Mongo Express credentials +ME_CONFIG_BASICAUTH_USERNAME=admin +ME_CONFIG_BASICAUTH_PASSWORD=password + +## Do not change anything below this line +ME_CONFIG_MONGODB_ADMINUSERNAME=${MONGO_INITDB_ROOT_USERNAME} +ME_CONFIG_MONGODB_ADMINPASSWORD=${MONGO_INITDB_ROOT_PASSWORD} +ME_CONFIG_MONGODB_URL=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@qn-history-service-mongo:27017/ + +MONGO_LOCAL_URI=mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@qn-history-service-mongo:27017/ diff --git a/backend/qn-history-service/Dockerfile b/backend/qn-history-service/Dockerfile new file mode 100644 index 0000000000..686853d5f5 --- /dev/null +++ b/backend/qn-history-service/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /qn-history-service + +COPY package*.json ./ + +RUN npm ci + +COPY . . + +EXPOSE 3005 + +CMD ["npm", "run", "dev"] diff --git a/backend/qn-history-service/README.md b/backend/qn-history-service/README.md new file mode 100644 index 0000000000..e1c3c7a644 --- /dev/null +++ b/backend/qn-history-service/README.md @@ -0,0 +1,29 @@ +# Question History Service Guide + +> Please ensure that you have completed the backend set-up [here](../README.md) before proceeding. + +## Setting-up Question History Service + +1. In the `qn-history-service` directory, create a copy of the `.env.sample` file and name it `.env`. + +2. To connect to your cloud MongoDB instead of your local MongoDB, set the `NODE_ENV` to `production` instead of `development`. + +3. Update `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` to change your MongoDB credentials if necessary. + +4. You can view the MongoDB collections locally using Mongo Express. To set up Mongo Express, update `ME_CONFIG_BASICAUTH_USERNAME` and `ME_CONFIG_BASICAUTH_PASSWORD`. The username and password will be the login credentials when you access Mongo Express at http://localhost:8083. + +## Running Question History Service without Docker + +> Make sure you have the cloud MongoDB URI in your .env file and set NODE_ENV to production already. + +1. Open Command Line/Terminal and navigate into the `qn-history-service` directory. + +2. Run the command: `npm install`. This will install all the necessary dependencies. + +3. Run the command `npm start` to start the Question History Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. + +## After running + +1. To view Question History Service documentation, go to http://localhost:3005/docs. + +2. Using applications like Postman, you can interact with the Question History Service on port 3005. If you wish to change this, please update the `.env` file. diff --git a/backend/qn-history-service/app.ts b/backend/qn-history-service/app.ts new file mode 100644 index 0000000000..4b02f33a84 --- /dev/null +++ b/backend/qn-history-service/app.ts @@ -0,0 +1,37 @@ +import express, { Request, Response } from "express"; +import dotenv from "dotenv"; +import fs from "fs"; +import yaml from "yaml"; +import swaggerUi from "swagger-ui-express"; +import cors from "cors"; + +import qnHistoryRoutes from "./src/routes/questionHistoryRoutes.ts"; + +dotenv.config(); + +export const allowedOrigins = process.env.ORIGINS + ? process.env.ORIGINS.split(",") + : ["http://localhost:5173", "http://127.0.0.1:5173"]; + +const file = fs.readFileSync("./swagger.yml", "utf-8"); +const swaggerDocument = yaml.parse(file); + +const app = express(); + +app.use(cors({ origin: allowedOrigins, credentials: true })); + +app.options("*", cors({ origin: allowedOrigins, credentials: true })); + +app.use(express.json()); + +app.use("/api/qnhistories", qnHistoryRoutes); + +app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + +app.get("/", (req: Request, res: Response) => { + res + .status(200) + .json({ message: "Hello world from question history service" }); +}); + +export default app; diff --git a/backend/qn-history-service/config/db.ts b/backend/qn-history-service/config/db.ts new file mode 100644 index 0000000000..e5918d0739 --- /dev/null +++ b/backend/qn-history-service/config/db.ts @@ -0,0 +1,24 @@ +import mongoose from "mongoose"; +import dotenv from "dotenv"; + +dotenv.config(); + +const connectDB = async () => { + try { + const mongoDBUri: string | undefined = + process.env.NODE_ENV === "production" + ? process.env.MONGO_CLOUD_URI + : process.env.MONGO_LOCAL_URI; + + if (!mongoDBUri) { + throw new Error("MongoDB URI is not provided"); + } + + await mongoose.connect(mongoDBUri); + } catch (error) { + console.error(error); + process.exit(1); + } +}; + +export default connectDB; diff --git a/backend/qn-history-service/eslint.config.js b/backend/qn-history-service/eslint.config.js new file mode 100644 index 0000000000..32d12d9801 --- /dev/null +++ b/backend/qn-history-service/eslint.config.js @@ -0,0 +1,22 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default [ + { files: ["**/*.{js,mjs,cjs,ts}"] }, + { languageOptions: { globals: globals.node } }, + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; diff --git a/backend/qn-history-service/jest.config.ts b/backend/qn-history-service/jest.config.ts new file mode 100644 index 0000000000..151d29ec19 --- /dev/null +++ b/backend/qn-history-service/jest.config.ts @@ -0,0 +1,199 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +import type { Config } from "jest"; + +const config: Config = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/04/ng4c26hj1ksdsy_7x_21kvx80000gn/T/jest_dx", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: undefined, + + // The directory where Jest should output its coverage files + coverageDirectory: "coverage", + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "v8", + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: undefined, + + // A path to a custom dependency extractor + // dependencyExtractor: undefined, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // The default configuration for fake timers + // fakeTimers: { + // "enableGlobally": false + // }, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: undefined, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: undefined, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "mjs", + // "cjs", + // "jsx", + // "ts", + // "tsx", + // "json", + // "node" + // ], + + // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + preset: "ts-jest", + + // Run tests from one or more projects + // projects: undefined, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state before every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: undefined, + + // Automatically restore mock state and implementation before every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: undefined, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // The number of seconds after which a test is considered as slow and reported as such in the results. + // slowTestThreshold: 5, + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + // testEnvironment: "jest-environment-node", + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: undefined, + + // This option allows use of a custom test runner + // testRunner: "jest-circus/runner", + + // A map from regular expressions to paths to transformers + // transform: undefined, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/", + // "\\.pnp\\.[^\\/]+$" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: undefined, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; + +export default config; diff --git a/backend/qn-history-service/package-lock.json b/backend/qn-history-service/package-lock.json new file mode 100644 index 0000000000..cd26990e48 --- /dev/null +++ b/backend/qn-history-service/package-lock.json @@ -0,0 +1,6692 @@ +{ + "name": "qn-history-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "qn-history-service", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.7.7", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "mongoose": "^8.7.3", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.6.0" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.8.1", + "@types/swagger-ui-express": "^4.1.6", + "cross-env": "^7.0.3", + "eslint": "^9.13.0", + "globals": "^15.11.0", + "jest": "^29.7.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3", + "typescript-eslint": "^8.11.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", + "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.0.tgz", + "integrity": "sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.0.tgz", + "integrity": "sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.0", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz", + "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz", + "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", + "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "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", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz", + "integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", + "integrity": "sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/type-utils": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz", + "integrity": "sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", + "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.11.0.tgz", + "integrity": "sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", + "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", + "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", + "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz", + "integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "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" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/bson": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.9.0.tgz", + "integrity": "sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001673", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz", + "integrity": "sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "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/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "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" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.47", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz", + "integrity": "sha512-zS5Yer0MOYw4rtK2iq43cJagHZ8sXN0jDHDKzB+86gSBSAI4v07S97mcq+Gs2vclAxSh1j7vOAHxSVgduiiuVQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", + "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.13.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-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" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz", + "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.3.tgz", + "integrity": "sha512-Xl6+dzU5ZpEcDoJ8/AyrIdAwTY099QwpolvV73PIytpK13XqwllLq/9XeVzzLEQgmyvwBVGVgjmMrKbuezxrIA==", + "license": "MIT", + "dependencies": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.9.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "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/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.11.0.tgz", + "integrity": "sha512-cBRGnW3FSlxaYwU8KfAewxFK5uzeOAp0l2KebIlPDOT5olVi65KDG/yjBooPBG0kGW/HLkoz1c/iuBFehcS3IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.11.0", + "@typescript-eslint/parser": "8.11.0", + "@typescript-eslint/utils": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.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/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/qn-history-service/package.json b/backend/qn-history-service/package.json new file mode 100644 index 0000000000..a5b6a7d4bd --- /dev/null +++ b/backend/qn-history-service/package.json @@ -0,0 +1,41 @@ +{ + "name": "qn-history-service", + "version": "1.0.0", + "main": "server.ts", + "type": "module", + "scripts": { + "start": "tsx server.ts", + "dev": "tsx watch server.ts", + "test": "cross-env NODE_ENV=test && jest", + "test:watch": "cross-env NODE_ENV=test && jest --watch", + "lint": "eslint ." + }, + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.8.1", + "@types/swagger-ui-express": "^4.1.6", + "cross-env": "^7.0.3", + "eslint": "^9.13.0", + "globals": "^15.11.0", + "jest": "^29.7.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3", + "typescript-eslint": "^8.11.0" + }, + "dependencies": { + "axios": "^1.7.7", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "mongoose": "^8.7.3", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.6.0" + } +} diff --git a/backend/qn-history-service/server.ts b/backend/qn-history-service/server.ts new file mode 100644 index 0000000000..0076a9289f --- /dev/null +++ b/backend/qn-history-service/server.ts @@ -0,0 +1,21 @@ +import app from "./app.ts"; +import connectDB from "./config/db.ts"; + +const PORT = process.env.SERVICE_PORT || 3005; + +if (process.env.NODE_ENV !== "test") { + connectDB() + .then(() => { + console.log("MongoDB Connected!"); + + app.listen(PORT, () => { + console.log( + `Question history service server listening on http://localhost:${PORT}` + ); + }); + }) + .catch((err) => { + console.error("Failed to connect to DB"); + console.error(err); + }); +} diff --git a/backend/qn-history-service/src/controllers/questionHistoryController.ts b/backend/qn-history-service/src/controllers/questionHistoryController.ts new file mode 100644 index 0000000000..3d115675d9 --- /dev/null +++ b/backend/qn-history-service/src/controllers/questionHistoryController.ts @@ -0,0 +1,195 @@ +import { Request, Response } from "express"; +import QnHistory, { IQnHistory } from "../models/QnHistory.ts"; +import { + MONGO_OBJ_ID_FORMAT, + MONGO_OBJ_ID_MALFORMED_MESSAGE, + PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE, + PAGE_LIMIT_USERID_REQUIRED_MESSAGE, + QN_HIST_CREATED_MESSAGE, + QN_HIST_DELETED_MESSAGE, + QN_HIST_NOT_FOUND_MESSAGE, + QN_HIST_RETRIEVED_MESSAGE, + SERVER_ERROR_MESSAGE, +} from "../utils/constants.ts"; + +export const createQnHistory = async ( + req: Request, + res: Response +): Promise => { + try { + const { + userIds, + questionId, + title, + submissionStatus, + dateAttempted, + timeTaken, + } = req.body; + console.log( + userIds, + questionId, + title, + submissionStatus, + dateAttempted, + timeTaken + ); + + const newQnHistory = new QnHistory({ + userIds, + questionId, + title, + submissionStatus, + dateAttempted, + timeTaken, + }); + + await newQnHistory.save(); + + res.status(201).json({ + message: QN_HIST_CREATED_MESSAGE, + qnHistory: formatQnHistoryResponse(newQnHistory), + }); + } catch (error) { + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); + } +}; + +export const updateQnHistory = async ( + req: Request, + res: Response +): Promise => { + try { + const { id } = req.params; + + if (!id.match(MONGO_OBJ_ID_FORMAT)) { + res.status(400).json({ message: MONGO_OBJ_ID_MALFORMED_MESSAGE }); + return; + } + + const currQnHistory = await QnHistory.findById(id); + + if (!currQnHistory) { + res.status(404).json({ message: QN_HIST_NOT_FOUND_MESSAGE }); + return; + } + + const updatedQnHistory = await QnHistory.findByIdAndUpdate(id, req.body, { + new: true, + }); + + res.status(200).json({ + message: "Question updated successfully", + qnHistory: formatQnHistoryResponse(updatedQnHistory as IQnHistory), + }); + } catch (error) { + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); + } +}; + +export const deleteQnHistory = async ( + req: Request, + res: Response +): Promise => { + try { + const { id } = req.params; + + if (!id.match(MONGO_OBJ_ID_FORMAT)) { + res.status(400).json({ message: MONGO_OBJ_ID_MALFORMED_MESSAGE }); + return; + } + + const currQnHistory = await QnHistory.findById(id); + if (!currQnHistory) { + res.status(404).json({ message: QN_HIST_NOT_FOUND_MESSAGE }); + return; + } + + await QnHistory.findByIdAndDelete(id); + res.status(200).json({ message: QN_HIST_DELETED_MESSAGE }); + } catch (error) { + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); + } +}; + +type QnHistListParams = { + page: string; + qnHistLimit: string; + userId: string; +}; + +export const readQnHistoryList = async ( + req: Request, + res: Response +): Promise => { + try { + const { page, qnHistLimit, userId } = req.query; + + if (!page || !qnHistLimit || !userId) { + res.status(400).json({ message: PAGE_LIMIT_USERID_REQUIRED_MESSAGE }); + return; + } + + const pageInt = parseInt(page, 10); + const qnHistLimitInt = parseInt(qnHistLimit, 10); + + if (pageInt < 1 || qnHistLimitInt < 1) { + res.status(400).json({ message: PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE }); + return; + } + + const filteredQnHistCount = await QnHistory.countDocuments({ + userIds: userId, + }); + const filteredQnHist = await QnHistory.find({ userIds: userId }) + .skip((pageInt - 1) * qnHistLimitInt) + .limit(qnHistLimitInt); + + res.status(200).json({ + message: QN_HIST_RETRIEVED_MESSAGE, + qnHistoryCount: filteredQnHistCount, + qnHistories: filteredQnHist.map(formatQnHistoryResponse), + }); + } catch (error) { + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); + } +}; + +export const readQnHistIndiv = async ( + req: Request, + res: Response +): Promise => { + try { + const { id } = req.params; + + if (!id.match(MONGO_OBJ_ID_FORMAT)) { + res.status(400).json({ message: MONGO_OBJ_ID_MALFORMED_MESSAGE }); + return; + } + + const qnHistDetails = await QnHistory.findById(id); + if (!qnHistDetails) { + res.status(404).json({ message: QN_HIST_NOT_FOUND_MESSAGE }); + return; + } + + res.status(200).json({ + message: QN_HIST_RETRIEVED_MESSAGE, + qnHistory: formatQnHistoryResponse(qnHistDetails), + }); + } catch (error) { + res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); + } +}; + +const formatQnHistoryResponse = (qnHistory: IQnHistory) => { + return { + id: qnHistory._id, + userIds: qnHistory.userIds, + questionId: qnHistory.questionId, + title: qnHistory.title, + submissionStatus: qnHistory.submissionStatus, + dateAttempted: qnHistory.dateAttempted, + timeTaken: qnHistory.timeTaken, + code: qnHistory.code, + }; +}; diff --git a/backend/qn-history-service/src/models/QnHistory.ts b/backend/qn-history-service/src/models/QnHistory.ts new file mode 100644 index 0000000000..7e41ee8fe6 --- /dev/null +++ b/backend/qn-history-service/src/models/QnHistory.ts @@ -0,0 +1,34 @@ +import mongoose, { Schema, Document } from "mongoose"; + +export interface IQnHistory extends Document { + userIds: string[]; + questionId: string; + title: string; + submissionStatus: string; + dateAttempted: Date; + timeTaken: Number; + code: string; + createdAt: Date; + updatedAt: Date; +} + +const qnHistorySchema: Schema = new mongoose.Schema( + { + userIds: { type: [String], required: true }, + questionId: { type: String, required: true }, + title: { type: String, required: true }, + submissionStatus: { + type: String, + enum: ["Accepted", "Rejected", "Attempted"], + required: true, + }, + dateAttempted: { type: Date, required: true }, + timeTaken: { type: Number, required: true }, + code: { type: String, required: false, default: "" }, + }, + { timestamps: true } +); + +const QnHistory = mongoose.model("QnHistory", qnHistorySchema); + +export default QnHistory; diff --git a/backend/qn-history-service/src/routes/questionHistoryRoutes.ts b/backend/qn-history-service/src/routes/questionHistoryRoutes.ts new file mode 100644 index 0000000000..4395263206 --- /dev/null +++ b/backend/qn-history-service/src/routes/questionHistoryRoutes.ts @@ -0,0 +1,22 @@ +import express from "express"; +import { + createQnHistory, + deleteQnHistory, + readQnHistIndiv, + readQnHistoryList, + updateQnHistory, +} from "../controllers/questionHistoryController"; + +const router = express.Router(); + +router.post("/", createQnHistory); + +router.put("/:id", updateQnHistory); + +router.get("/", readQnHistoryList); + +router.get("/:id", readQnHistIndiv); + +router.delete("/:id", deleteQnHistory); + +export default router; diff --git a/backend/qn-history-service/src/utils/constants.ts b/backend/qn-history-service/src/utils/constants.ts new file mode 100644 index 0000000000..8f8fbd3097 --- /dev/null +++ b/backend/qn-history-service/src/utils/constants.ts @@ -0,0 +1,21 @@ +export const QN_HIST_CREATED_MESSAGE = "Question history created successfully."; + +export const QN_HIST_NOT_FOUND_MESSAGE = "Question history not found."; + +export const QN_HIST_DELETED_MESSAGE = "Question history deleted successfully."; + +export const SERVER_ERROR_MESSAGE = "Server error."; + +export const QN_HIST_RETRIEVED_MESSAGE = + "Question history retrieved successfully."; + +export const PAGE_LIMIT_USERID_REQUIRED_MESSAGE = + "Page number, question limit per page and userId should be provided."; + +export const PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE = + "Page number and question limit per page should be positive integers."; + +export const MONGO_OBJ_ID_FORMAT = /^[0-9a-fA-F]{24}$/; + +export const MONGO_OBJ_ID_MALFORMED_MESSAGE = + "The question history ID is not valid"; diff --git a/backend/qn-history-service/swagger.yml b/backend/qn-history-service/swagger.yml new file mode 100644 index 0000000000..cc14bec8f6 --- /dev/null +++ b/backend/qn-history-service/swagger.yml @@ -0,0 +1,306 @@ +openapi: 3.0.0 + +info: + title: Question History Service + version: 1.0.0 + +components: + schemas: + QnHistory: + properties: + userIds: + type: array + items: + type: string + description: User IDs + questionId: + type: string + description: Question ID + title: + type: string + description: Question title + submissionStatus: + type: string + description: Code submission status + dateAttempted: + type: string + format: date + description: Date that question was attempted + timeTaken: + type: number + description: Time taken for question attempt in minutes + code: + type: string + description: Code submitted + +definitions: + QnHistory: + type: object + properties: + _id: + type: string + description: Question history ID + userIds: + type: array + items: + type: string + description: User IDs + questionId: + type: string + description: Question ID + title: + type: string + description: Question title + submissionStatus: + type: string + description: Code submission status + dateAttempted: + type: string + format: date + description: Date that question was attempted + timeTaken: + type: number + description: Time taken for question attempt in minutes + code: + type: string + description: Code submitted + createdAt: + type: string + description: Date of creation + updatedAt: + type: string + description: Latest update + __v: + type: string + description: Document version + Error: + type: object + properties: + message: + type: string + description: Message + ServerError: + type: object + properties: + message: + type: string + description: Message + error: + type: string + description: Error + +paths: + /: + get: + tags: + - root + summary: Root + description: Ping the server + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + /api/qnhistories: + post: + tags: + - qnhistories + summary: Creates a question history + description: Creates a question history + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/QnHistory" + responses: + 201: + description: Created question history + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + qnHistory: + $ref: "#/definitions/QnHistory" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/definitions/ServerError" + get: + tags: + - qnhistories + summary: Reads a list of question histories + description: Reads a limited list of question histories based on current page and limit per page + parameters: + - in: query + name: page + type: integer + required: true + default: 1 + description: Page of question histories to return + - in: query + name: qnHistLimit + type: integer + required: true + default: 10 + description: Limit on number of question histories to return + - in: query + name: userId + type: string + required: true + description: User id of user to retrieve question histories + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + qnHistoryCount: + type: integer + description: Total number of question histories + qnHistories: + type: array + items: + $ref: "#/definitions/QnHistory" + 400: + description: Bad Request + content: + application/json: + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/definitions/ServerError" + /api/qnhistories/{id}: + put: + tags: + - qnhistories + summary: Updates a question history + description: Updates a question history + parameters: + - in: path + name: id + type: string + required: true + description: Question history id + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/QnHistory" + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + qnHistory: + $ref: "#/definitions/QnHistory" + 404: + description: Question History Not Found + content: + application/json: + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/definitions/ServerError" + delete: + tags: + - qnhistories + summary: Deletes a question history + description: Deletes a question history + parameters: + - in: path + name: id + type: string + required: true + description: Question history id + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + 404: + description: Question History Not Found + content: + application/json: + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/definitions/ServerError" + get: + tags: + - qnhistories + summary: Reads a question history + description: Reads a question history + parameters: + - in: path + name: id + type: string + required: true + description: Question history id + responses: + 200: + description: Successful Response + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Message + qnHistory: + $ref: "#/definitions/QnHistory" + 404: + description: Question History Not Found + content: + application/json: + schema: + $ref: "#/definitions/Error" + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: "#/definitions/ServerError" diff --git a/backend/qn-history-service/tsconfig.json b/backend/qn-history-service/tsconfig.json new file mode 100644 index 0000000000..2d10546bb3 --- /dev/null +++ b/backend/qn-history-service/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 629a715847..eb3e1940b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,6 +70,25 @@ services: - /collab-service/node_modules restart: on-failure + qn-history-service: + image: peerprep/qn-history-service + build: ./backend/qn-history-service + environment: + - CHOKIDAR_USEPOLLING=true + env_file: ./backend/qn-history-service/.env + ports: + - 3005:3005 + depends_on: + - qn-history-service-mongo + - user-service + - question-service + networks: + - peerprep-network + volumes: + - ./backend/qn-history-service:/qn-history-service + - /qn-history-service/node_modules + restart: on-failure + frontend: image: peerprep/frontend build: ./frontend @@ -82,6 +101,7 @@ services: - question-service - matching-service - collab-service + - qn-history-service networks: - peerprep-network volumes: @@ -135,6 +155,29 @@ services: - user-service-mongo env_file: ./backend/user-service/.env + qn-history-service-mongo: + image: mongo + restart: always + ports: + - 27019:27017 + networks: + - peerprep-network + volumes: + - qn-history-service-mongo-data:/data/db + env_file: + - ./backend/qn-history-service/.env + + qn-history-service-mongo-express: + image: mongo-express + restart: always + ports: + - 8083:8081 + networks: + - peerprep-network + depends_on: + - qn-history-service-mongo + env_file: ./backend/qn-history-service/.env + rabbitmq: image: rabbitmq:4.0-management container_name: rabbitmq @@ -184,6 +227,7 @@ services: volumes: question-service-mongo-data: user-service-mongo-data: + qn-history-service-mongo-data: redis-data: redis-insight-data: diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 57a4ecb1cd..b6b16293c3 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -18,6 +18,9 @@ import useAppNavigate from "../components/UseAppNavigate"; import { UNSAFE_NavigationContext } from "react-router-dom"; import { Action, type History, type Transition } from "history"; +let matchUserId: string; +let partnerUserId: string; + type MatchUser = { id: string; username: string; @@ -92,6 +95,7 @@ type MatchContextType = { loading: boolean; isEndSessionModalOpen: boolean; questionId: string | null; + qnHistoryId: string | null; }; const requestTimeoutDuration = 5000; @@ -117,6 +121,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [matchPending, setMatchPending] = useState(false); const [loading, setLoading] = useState(true); const [questionId, setQuestionId] = useState(null); + const [qnHistoryId, setQnHistoryId] = useState(null); const [isEndSessionModalOpen, setIsEndSessionModalOpen] = useState(false); @@ -130,8 +135,10 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { username: user.username, profile: user.profilePictureUrl, }); + matchUserId = user.id; } else { setMatchUser(null); + matchUserId = ""; } }, [user]); @@ -185,6 +192,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } setMatchId(null); setPartner(null); + partnerUserId = ""; setMatchPending(false); setLoading(false); }; @@ -279,10 +287,11 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const initMatchedListeners = () => { - matchSocket.on(MatchEvents.MATCH_SUCCESSFUL, (id: string) => { + matchSocket.on(MatchEvents.MATCH_SUCCESSFUL, (qnId: string, qnHistId: string) => { setMatchPending(false); appNavigate(MatchPaths.COLLAB); - setQuestionId(id); + setQuestionId(qnId); + setQnHistoryId(qnHistId); }); matchSocket.on(MatchEvents.MATCH_UNSUCCESSFUL, () => { @@ -314,8 +323,10 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setMatchId(matchId); if (matchUser?.id === user1.id) { setPartner(user2); + partnerUserId = user2.id; } else { setPartner(user1); + partnerUserId = user1.id; } setMatchPending(true); appNavigate(MatchPaths.MATCHED); @@ -395,7 +406,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const acceptMatch = () => { - matchSocket.emit(MatchEvents.MATCH_ACCEPT_REQUEST, matchId); + matchSocket.emit(MatchEvents.MATCH_ACCEPT_REQUEST, matchId, matchUserId, partnerUserId); }; const rematch = () => { @@ -430,6 +441,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { if (requested) { appNavigate(MatchPaths.MATCHING); setPartner(null); + partnerUserId = ""; } } ); @@ -479,9 +491,11 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { if (match) { setMatchId(match.matchId); setPartner(match.partner); + partnerUserId = match.partner.id; } else { setMatchId(null); setPartner(null); + partnerUserId = ""; } setLoading(false); } @@ -527,6 +541,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { loading, isEndSessionModalOpen, questionId, + qnHistoryId, }} > {children} diff --git a/frontend/src/pages/Profile/index.tsx b/frontend/src/pages/Profile/index.tsx index 896c645b4f..c616c3ab62 100644 --- a/frontend/src/pages/Profile/index.tsx +++ b/frontend/src/pages/Profile/index.tsx @@ -1,9 +1,9 @@ import { useParams } from "react-router-dom"; import AppMargin from "../../components/AppMargin"; import ProfileDetails from "../../components/ProfileDetails"; -import { Box, Button, Divider, Stack, Typography } from "@mui/material"; +import { Box, Button, Divider, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, Typography } from "@mui/material"; import classes from "./index.module.css"; -import { useEffect } from "react"; +import { useEffect, useReducer, useState } from "react"; import { useAuth } from "../../contexts/AuthContext"; import ServerError from "../../components/ServerError"; import EditProfileModal from "../../components/EditProfileModal"; @@ -13,8 +13,16 @@ import { USE_AUTH_ERROR_MESSAGE, USE_PROFILE_ERROR_MESSAGE, } from "../../utils/constants"; +import qnHistoryReducer, { getQnHistoryList, initialQHState } from "../../reducers/qnHistoryReducer"; +import { grey } from "@mui/material/colors"; +import { convertDateString } from "../../utils/sessionTime"; + +const rowsPerPage = 10; const ProfilePage: React.FC = () => { + const [page, setPage] = useState(0); + const [state, dispatch] = useReducer(qnHistoryReducer, initialQHState); + const { userId } = useParams<{ userId: string }>(); const auth = useAuth(); @@ -46,6 +54,20 @@ const ProfilePage: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const updateQnHistoryList = () => { + if (userId) { + getQnHistoryList( + page + 1, // convert from 0-based indexing + rowsPerPage, + userId, + dispatch + ); + } + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => updateQnHistoryList(), [page]); + if (!user) { return ( { const isCurrentUser = auth.user?.id === userId; + const tableHeaders = ["Title", "Status", "Date submitted"]; + return ( { )}
+ ({ flex: 3, paddingLeft: theme.spacing(4) })}> - Questions attempted + Questions attempted + + ({ + "& .MuiTableCell-root": { padding: theme.spacing(1.2) }, + whiteSpace: "nowrap", + })} + > + + + {tableHeaders.map((header) => ( + + + {header} + + + ))} + + + + {state.qnHistories.slice(0, rowsPerPage).map((qnHistory) => ( + + + {}} + > + {qnHistory.title} + + + + + {qnHistory.submissionStatus} + + + + + {convertDateString(qnHistory.dateAttempted)} + + + + ))} + +
+
+ setPage(page)} + />
{editProfileOpen && ( ; + questionId: string; + title: string; + submissionStatus: string; + dateAttempted: string; + timeTaken: number; + code: string; +}; + +type QnHistoryList = { + qnHistories: Array; + qnHistoryCount: number; +}; + +enum QnHistoryActionTypes { + CREATE_QNHIST = "create_qnhist", + VIEW_QNHIST_LIST = "view_qnhist_list", + VIEW_QNHIST = "view_qnhist", + UPDATE_QNHIST = "update_qnhist", + ERROR_CREATING_QNHIST = "error_creating_qnhist", + ERROR_FETCHING_QNHIST_LIST = "error_fetching_qnhist_list", + ERROR_FETCHING_SELECTED_QNHIST = "error_fetching_selected_qnhist", + ERROR_UPDATING_QNHIST = "error_updating_qnhist", +} + +type QnHistoryActions = { + type: QnHistoryActionTypes; + payload: QnHistoryList | QnHistoryDetail | string[] | string; +}; + +type QnHistoriesState = { + qnHistories: Array; + qnHistoryCount: number; + selectedQnHistory: QnHistoryDetail | null; + qnHistoryListError: string | null; + selectedQnHistoryError: string | null; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const isQnHistory = (qnHistory: any): qnHistory is QnHistoryDetail => { + if (!qnHistory || typeof qnHistory !== "object") { + return false; + } + + return ( + isString(qnHistory.id) && + isStringArray(qnHistory.userIds) && + isString(qnHistory.questionId) && + isString(qnHistory.title) && + isString(qnHistory.submissionStatus) && + isString(qnHistory.dateAttempted) && + Object.prototype.toString.call(new Date(qnHistory.dateAttempted)) === + "[object Date]" && + !isNaN(new Date(qnHistory.dateAttempted).getTime()) && + typeof qnHistory.timeTaken === "number" && + isString(qnHistory.code) + ); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const isQnHistoryList = ( + qnHistoryList: any +): qnHistoryList is QnHistoryList => { + if (!qnHistoryList || typeof qnHistoryList !== "object") { + return false; + } + + return ( + Array.isArray(qnHistoryList.qnHistories) && + // eslint-disable-next-line @typescript-eslint/no-explicit-any + qnHistoryList.qnHistories.every((qnHistory: any) => + isQnHistory(qnHistory) + ) && + typeof qnHistoryList.qnHistoryCount === "number" + ); +}; + +export const initialQHState: QnHistoriesState = { + qnHistories: [], + qnHistoryCount: 0, + selectedQnHistory: null, + qnHistoryListError: null, + selectedQnHistoryError: null, +}; + +export const createQnHistory = async ( + qnHistory: Omit, + dispatch: Dispatch +): Promise => { + const accessToken = localStorage.getItem("token"); + return qnHistoryClient + .post( + "/", + { + userIds: qnHistory.userIds, + questionId: qnHistory.questionId, + title: qnHistory.title, + submissionStatus: qnHistory.submissionStatus, + dateAttempted: qnHistory.dateAttempted, + timeTaken: qnHistory.timeTaken, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + .then((res) => { + dispatch({ + type: QnHistoryActionTypes.CREATE_QNHIST, + payload: res.data, + }); + return res.data.qnHistory.id; + }) + .catch((err) => { + dispatch({ + type: QnHistoryActionTypes.ERROR_CREATING_QNHIST, + payload: err.response?.data.message || err.message, + }); + return ""; + }); +}; + +export const getQnHistoryList = ( + page: number, + qnHistLimit: number, + userId: string, + dispatch: Dispatch +) => { + qnHistoryClient + .get("", { + params: { + page: page, + qnHistLimit: qnHistLimit, + userId: userId, + }, + }) + .then((res) => { + console.log(res.data); + dispatch({ + type: QnHistoryActionTypes.VIEW_QNHIST_LIST, + payload: res.data, + }); + }) + .catch((err) => + dispatch({ + type: QnHistoryActionTypes.ERROR_FETCHING_QNHIST_LIST, + payload: err.response?.data.message || err.message, + }) + ); +}; + +export const getQnHistoryById = ( + qnHistoryId: string, + dispatch: Dispatch +) => { + qnHistoryClient + .get(`/${qnHistoryId}`) + .then((res) => { + dispatch({ + type: QnHistoryActionTypes.VIEW_QNHIST, + payload: res.data.qnHistory, + }); + }) + .catch((err) => + dispatch({ + type: QnHistoryActionTypes.ERROR_FETCHING_SELECTED_QNHIST, + payload: err.response?.data.message || err.message, + }) + ); +}; + +export const updateQnHistoryById = async ( + qnHistoryId: string, + qnHistory: Omit, + dispatch: Dispatch +): Promise => { + const accessToken = localStorage.getItem("token"); + return qnHistoryClient + .put( + `/${qnHistoryId}`, + { + submissionStatus: qnHistory.submissionStatus, + dateAttempted: qnHistory.dateAttempted, + timeTaken: qnHistory.timeTaken, + code: qnHistory.code, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) + .then((res) => { + dispatch({ + type: QnHistoryActionTypes.UPDATE_QNHIST, + payload: res.data, + }); + return true; + }) + .catch((err) => { + dispatch({ + type: QnHistoryActionTypes.ERROR_UPDATING_QNHIST, + payload: err.response?.data.message || err.message, + }); + return false; + }); +}; + +export const deleteQuestionById = async (qnHistoryId: string) => { + try { + const accessToken = localStorage.getItem("token"); + await qnHistoryClient.delete(`/${qnHistoryId}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return true; + } catch { + return false; + } +}; + +export const setSelectedQnHistoryError = ( + error: string, + dispatch: React.Dispatch +) => { + dispatch({ + type: QnHistoryActionTypes.ERROR_FETCHING_SELECTED_QNHIST, + payload: error, + }); +}; + +const qnHistoryReducer = ( + state: QnHistoriesState, + action: QnHistoryActions +): QnHistoriesState => { + const { type } = action; + + switch (type) { + case QnHistoryActionTypes.CREATE_QNHIST: { + const { payload } = action; + if (!isQnHistory(payload)) { + return state; + } + return { ...state, qnHistories: [payload, ...state.qnHistories] }; + } + case QnHistoryActionTypes.VIEW_QNHIST_LIST: { + const { payload } = action; + if (!isQnHistoryList(payload)) { + return state; + } + return { + ...state, + qnHistories: payload.qnHistories, + qnHistoryCount: payload.qnHistoryCount, + }; + } + case QnHistoryActionTypes.VIEW_QNHIST: { + const { payload } = action; + if (!isQnHistory(payload)) { + return state; + } + return { ...state, selectedQnHistory: payload }; + } + case QnHistoryActionTypes.UPDATE_QNHIST: { + const { payload } = action; + if (!isQnHistory(payload)) { + return state; + } + return { + ...state, + qnHistories: state.qnHistories.map((qnHistory) => + qnHistory.id === payload.id ? payload : qnHistory + ), + }; + } + case QnHistoryActionTypes.ERROR_CREATING_QNHIST: { + const { payload } = action; + if (!isString(payload)) { + return state; + } + return { ...state, selectedQnHistoryError: payload }; + } + case QnHistoryActionTypes.ERROR_FETCHING_QNHIST_LIST: { + const { payload } = action; + if (!isString(payload)) { + return state; + } + return { ...state, qnHistoryListError: payload }; + } + case QnHistoryActionTypes.ERROR_FETCHING_SELECTED_QNHIST: { + const { payload } = action; + if (!isString(payload)) { + return state; + } + return { ...state, selectedQnHistoryError: payload }; + } + case QnHistoryActionTypes.ERROR_UPDATING_QNHIST: { + const { payload } = action; + if (!isString(payload)) { + return state; + } + return { ...state, selectedQnHistoryError: payload }; + } + default: + return state; + } +}; + +export default qnHistoryReducer; diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index bfbcffbe4f..18e283f415 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -2,6 +2,7 @@ import axios from "axios"; const usersUrl = "http://localhost:3001/api"; const questionsUrl = "http://localhost:3000/api/questions"; +const qnHistoriesUrl = "http://localhost:3005/api/qnhistories"; export const questionClient = axios.create({ baseURL: questionsUrl, @@ -12,3 +13,8 @@ export const userClient = axios.create({ baseURL: usersUrl, withCredentials: true, }); + +export const qnHistoryClient = axios.create({ + baseURL: qnHistoriesUrl, + withCredentials: true, +}); diff --git a/frontend/src/utils/sessionTime.ts b/frontend/src/utils/sessionTime.ts index a5802acb62..625ef18254 100644 --- a/frontend/src/utils/sessionTime.ts +++ b/frontend/src/utils/sessionTime.ts @@ -4,3 +4,13 @@ export const extractMinutesFromTime = (time: number) => Math.floor((time % 3600) / 60); // after extracting hours export const extractSecondsFromTime = (time: number) => time % 60; // after extracting hours and minutes + +export const extractMinutesOnly = (time: number) => time / 60; + +export const convertDateString = (date: string): string => { + return new Date(date).toLocaleDateString("en-GB", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +}; From 558fa213a700b288345bdaf0833169f1a22d9c5b Mon Sep 17 00:00:00 2001 From: jolynloh Date: Thu, 31 Oct 2024 15:29:23 +0800 Subject: [PATCH 055/192] Implement code template input fields on frontend --- .../QuestionCodeTemplates/index.tsx | 127 ++++++++++++++++++ .../QuestionFileContainer/index.tsx | 1 - frontend/src/pages/NewQuestion/index.tsx | 38 +++--- frontend/src/utils/constants.ts | 3 +- 4 files changed, 145 insertions(+), 24 deletions(-) create mode 100644 frontend/src/components/QuestionCodeTemplates/index.tsx diff --git a/frontend/src/components/QuestionCodeTemplates/index.tsx b/frontend/src/components/QuestionCodeTemplates/index.tsx new file mode 100644 index 0000000000..354687172e --- /dev/null +++ b/frontend/src/components/QuestionCodeTemplates/index.tsx @@ -0,0 +1,127 @@ +import { HelpOutlined } from "@mui/icons-material"; +import { + Box, + IconButton, + Stack, + TextField, + ToggleButton, + ToggleButtonGroup, + Tooltip, + Typography, +} from "@mui/material"; +import { useState } from "react"; +import { CODE_TEMPLATES_TOOLTIP_MESSAGE } from "../../utils/constants"; + +interface QuestionCodeTemplatesProps { + codeTemplates: { + [key: string]: string; + }; + setCodeTemplates: React.Dispatch< + React.SetStateAction<{ + [key: string]: string; + }> + >; +} + +const QuestionCodeTemplates: React.FC = ({ + codeTemplates, + setCodeTemplates, +}) => { + const [selectedLanguage, setSelectedLanguage] = useState("python"); + + const handleLanguageChange = ( + event: React.MouseEvent, + language: string + ) => { + if (language) { + setSelectedLanguage(language); + } + }; + + const handleCodeChange = (event: React.ChangeEvent) => { + const { value } = event.target; + setCodeTemplates((prevTemplates) => ({ + ...prevTemplates, + [selectedLanguage]: value, + })); + }; + + const handleTabKeys = (event: any) => { + const { value } = event.target; + + if (event.key === "Tab") { + event.preventDefault(); + + const cursorPosition = event.target.selectionStart; + const cursorEndPosition = event.target.selectionEnd; + const tab = "\t"; + + event.target.value = + value.substring(0, cursorPosition) + + tab + + value.substring(cursorEndPosition); + + event.target.selectionStart = cursorPosition + 1; + event.target.selectionEnd = cursorPosition + 1; + } + }; + + return ( + + + Code Templates + + + + } + placement="right" + arrow + > + + + + + + + Python + Java + C + + + + + ); +}; + +export default QuestionCodeTemplates; diff --git a/frontend/src/components/QuestionFileContainer/index.tsx b/frontend/src/components/QuestionFileContainer/index.tsx index ed53647adb..b045c5f85e 100644 --- a/frontend/src/components/QuestionFileContainer/index.tsx +++ b/frontend/src/components/QuestionFileContainer/index.tsx @@ -1,6 +1,5 @@ import FileUploadIcon from "@mui/icons-material/FileUpload"; import { Button, styled } from "@mui/material"; -import { useState } from "react"; import { toast } from "react-toastify"; interface QuestionFileContainerProps { diff --git a/frontend/src/pages/NewQuestion/index.tsx b/frontend/src/pages/NewQuestion/index.tsx index 7b68524c20..f1b5668b73 100644 --- a/frontend/src/pages/NewQuestion/index.tsx +++ b/frontend/src/pages/NewQuestion/index.tsx @@ -31,6 +31,7 @@ import QuestionTestCases, { } from "../../components/QuestionTestCases"; import { v4 as uuidv4 } from "uuid"; import QuestionTestCasesFileUpload from "../../components/QuestionTestCasesFileUpload"; +import QuestionCodeTemplates from "../../components/QuestionCodeTemplates"; const NewQuestion = () => { const navigate = useNavigate(); @@ -54,9 +55,13 @@ const NewQuestion = () => { null ); - const [pythonTemplate, setPythonTemplate] = useState(""); - const [javaTemplate, setJavaTemplate] = useState(""); - const [cTemplate, setCTemplate] = useState(""); + const [codeTemplates, setCodeTemplates] = useState<{ [key: string]: string }>( + { + python: "", + java: "", + c: "", + } + ); const handleBack = () => { if ( @@ -83,7 +88,8 @@ const NewQuestion = () => { testCase.input.trim() === "" || testCase.expectedOutput.trim() === "" ) || testcaseInputFile === null || - testcaseOutputFile === null + testcaseOutputFile === null || + Object.values(codeTemplates).some((value) => value === "") ) { toast.error(FILL_ALL_FIELDS); return; @@ -95,9 +101,9 @@ const NewQuestion = () => { // description: markdownText, // complexity: selectedComplexity, // categories: selectedCategories, - // pythonTemplate, - // javaTemplate, - // cTemplate, + // pythonTemplate: codeTemplates.python, + // javaTemplate: codeTemplates.java, + // cTemplate: codeTemplates.c, // }, // dispatch // ); @@ -175,21 +181,9 @@ const NewQuestion = () => { setTestcaseOutputFile={setTestcaseOutputFile} /> - {/* for the FE ppl to redesign... */} - setPythonTemplate(e.target.value)} - /> - setJavaTemplate(e.target.value)} - /> - setCTemplate(e.target.value)} + )} diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index b0f59fab8c..2163a4b92b 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -116,4 +116,5 @@ export const COLLABORATIVE_EDITOR_PATH = "/collaborative_editor.png"; /* Tooltips */ export const ADD_QUESTION_TEST_CASE_TOOLTIP_MESSAGE = `Add at least 1 and at most 3 test cases.
This will be displayed to users.`; -export const ADD_TEST_CASE_FILES_TOOLTIP_MESSAGE = `Upload files for executing test cases when user submits code.

This is a required field. Only text files accepted.`; +export const ADD_TEST_CASE_FILES_TOOLTIP_MESSAGE = `Upload files for executing test cases backend when user submits code.

This is a required field.
Only text files accepted.`; +export const CODE_TEMPLATES_TOOLTIP_MESSAGE = `This is a required field.
Fill in a code template for each language.`; From 2ea2620bf56b47978e4d82f8800b4e9cc0f425d1 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:54:11 +0800 Subject: [PATCH 056/192] Add question history detail page --- .github/workflows/ci.yml | 1 + README.md | 1 + .../src/handlers/websocketHandler.ts | 3 +- backend/qn-history-service/.env.sample | 2 + .../controllers/questionHistoryController.ts | 11 +- .../src/models/QnHistory.ts | 6 + backend/qn-history-service/swagger.yml | 6 + docker-compose-test.yml | 17 ++ frontend/src/App.tsx | 9 + frontend/src/components/ServerError/index.tsx | 2 +- frontend/src/pages/Profile/index.tsx | 41 ++-- .../src/pages/QuestionHistoryDetail/index.tsx | 187 ++++++++++++++++++ frontend/src/reducers/qnHistoryReducer.ts | 63 +++--- 13 files changed, 288 insertions(+), 61 deletions(-) create mode 100644 frontend/src/pages/QuestionHistoryDetail/index.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27ccd7ab04..9a45e654a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,7 @@ jobs: matching-service, collab-service, code-execution-service, + qn-history-service, ] steps: - name: Checkout code diff --git a/README.md b/README.md index 7f05cfc064..2a4964045e 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,5 @@ docker-compose down - Matching Service: http://localhost:3002 - Collab Service: http://localhost:3003 - Code Execution Service: http://localhost:3004 +- Question History Service: http://localhost:3005 - Frontend: http://localhost:5173 diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index ddcbd1f91b..2093b5de48 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -130,7 +130,7 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { return; } - const { complexity, category } = match; + const { complexity, category, language } = match; questionService .get("/random", { params: { complexity, category } }) .then((res) => { @@ -143,6 +143,7 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { submissionStatus: "Attempted", dateAttempted: new Date(), timeTaken: 0, + language: language, }) .then((res) => { io.to(matchId).emit( diff --git a/backend/qn-history-service/.env.sample b/backend/qn-history-service/.env.sample index 7c9f085eac..fa12352eca 100644 --- a/backend/qn-history-service/.env.sample +++ b/backend/qn-history-service/.env.sample @@ -6,6 +6,8 @@ ORIGINS=http://localhost:5173,http://127.0.0.1:5173 # if using cloud MongoDB, replace with actual URI (run service separately) MONGO_CLOUD_URI= +MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ + # if using local MongoDB (run service with docker-compose) ## MongoDB credentials MONGO_INITDB_ROOT_USERNAME=root diff --git a/backend/qn-history-service/src/controllers/questionHistoryController.ts b/backend/qn-history-service/src/controllers/questionHistoryController.ts index 3d115675d9..ee68c85317 100644 --- a/backend/qn-history-service/src/controllers/questionHistoryController.ts +++ b/backend/qn-history-service/src/controllers/questionHistoryController.ts @@ -24,15 +24,8 @@ export const createQnHistory = async ( submissionStatus, dateAttempted, timeTaken, + language, } = req.body; - console.log( - userIds, - questionId, - title, - submissionStatus, - dateAttempted, - timeTaken - ); const newQnHistory = new QnHistory({ userIds, @@ -41,6 +34,7 @@ export const createQnHistory = async ( submissionStatus, dateAttempted, timeTaken, + language, }); await newQnHistory.save(); @@ -191,5 +185,6 @@ const formatQnHistoryResponse = (qnHistory: IQnHistory) => { dateAttempted: qnHistory.dateAttempted, timeTaken: qnHistory.timeTaken, code: qnHistory.code, + language: qnHistory.language, }; }; diff --git a/backend/qn-history-service/src/models/QnHistory.ts b/backend/qn-history-service/src/models/QnHistory.ts index 7e41ee8fe6..f5805ae0ae 100644 --- a/backend/qn-history-service/src/models/QnHistory.ts +++ b/backend/qn-history-service/src/models/QnHistory.ts @@ -8,6 +8,7 @@ export interface IQnHistory extends Document { dateAttempted: Date; timeTaken: Number; code: string; + language: string; createdAt: Date; updatedAt: Date; } @@ -25,6 +26,11 @@ const qnHistorySchema: Schema = new mongoose.Schema( dateAttempted: { type: Date, required: true }, timeTaken: { type: Number, required: true }, code: { type: String, required: false, default: "" }, + language: { + type: String, + enum: ["Python", "Java", "C"], + required: true, + }, }, { timestamps: true } ); diff --git a/backend/qn-history-service/swagger.yml b/backend/qn-history-service/swagger.yml index cc14bec8f6..5818777a8a 100644 --- a/backend/qn-history-service/swagger.yml +++ b/backend/qn-history-service/swagger.yml @@ -32,6 +32,9 @@ components: code: type: string description: Code submitted + language: + type: string + description: Programming language used definitions: QnHistory: @@ -64,6 +67,9 @@ definitions: code: type: string description: Code submitted + language: + type: string + description: Programming language used createdAt: type: string description: Date of creation diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 7e5ea5e7df..10dc518c3a 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -98,6 +98,23 @@ services: restart: on-failure command: ["npm", "test"] + test-qn-history-service: + image: peerprep/qn-history-service + build: ./backend/qn-history-service + environment: + - NODE_ENV=test + - SERVICE_PORT=3005 + - MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ + depends_on: + - test-mongo + networks: + - peerprep-network + volumes: + - ./backend/qn-history-service:/qn-history-service + - /qn-history-service/node_modules + restart: on-failure + command: ["npm", "test"] + test-frontend: image: peerprep/frontend build: ./frontend diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 15f1fd5095..db2d37d413 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,6 +24,7 @@ import MatchProvider from "./contexts/MatchContext"; import CollabSandbox from "./pages/CollabSandbox"; import NoDirectAccessRoutes from "./components/NoDirectAccessRoutes"; import EmailVerification from "./pages/EmailVerification"; +import QuestionHistoryDetail from "./pages/QuestionHistoryDetail"; function App() { return ( @@ -51,6 +52,14 @@ function App() { } /> + + + + } + /> }> }> } /> diff --git a/frontend/src/components/ServerError/index.tsx b/frontend/src/components/ServerError/index.tsx index 29a2900c47..218dea7383 100644 --- a/frontend/src/components/ServerError/index.tsx +++ b/frontend/src/components/ServerError/index.tsx @@ -9,7 +9,7 @@ const ServerError: React.FC = (props) => { return ( - + ({marginTop: theme.spacing(4) })}> { const [page, setPage] = useState(0); const [state, dispatch] = useReducer(qnHistoryReducer, initialQHState); + const navigate = useNavigate(); const { userId } = useParams<{ userId: string }>(); const auth = useAuth(); @@ -52,7 +53,7 @@ const ProfilePage: React.FC = () => { fetchUser(userId); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [userId]); const updateQnHistoryList = () => { if (userId) { @@ -160,15 +161,22 @@ const ProfilePage: React.FC = () => { textOverflow: "ellipsis", }} > - {}} - > - {qnHistory.title} - + {isCurrentUser + ? navigate(`${qnHistory.id}`)} + > + {qnHistory.title} + + : + {qnHistory.title} + + } { page={page} onPageChange={(_, page) => setPage(page)} /> + {state.qnHistories.length === 0 && ( + + + There are currently no records. + + + )} {editProfileOpen && ( { + const { qnHistoryId } = useParams<{ qnHistoryId: string }>(); + const [qnhistState, qnhistDispatch] = useReducer(qnHistoryReducer, initialQHState); + const [qnState, qnDispatch] = useReducer(reducer, initialState); + const navigate = useNavigate(); + const auth = useAuth(); + + if (!auth) { + throw new Error(USE_AUTH_ERROR_MESSAGE); + } + + const { user } = auth; + + const tableHeaders = ["Status", "Date submitted", "Time taken", "Partner"] + + useEffect(() => { + if (!qnHistoryId) { + setSelectedQnHistoryError("Unable to fetch question history.", qnhistDispatch); + return; + } + + getQnHistoryById(qnHistoryId, qnhistDispatch); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (qnhistState.selectedQnHistory) { + getQuestionById(qnhistState.selectedQnHistory.questionId, qnDispatch); + } + }, [qnhistState]) + + + const getPartnerId = (userIds: string[], currUserId: string): string => { + if (currUserId == userIds[0]) { + return userIds[1]; + } else { + return userIds[0]; + } + } + + if (!qnhistState.selectedQnHistory) { + if (qnhistState.selectedQnHistoryError) { + return ( + + ); + } else { + return; + } + } + + const partnerId = user && qnhistState.selectedQnHistory && getPartnerId(qnhistState.selectedQnHistory.userIds, user.id); + + return ( + + navigate(-1)}> + + + { user && qnhistState.selectedQnHistory && + + ({ + "& .MuiTableCell-root": { padding: theme.spacing(1.2) }, + whiteSpace: "nowrap", + })} + > + + + {tableHeaders.map((header) => ( + + + {header} + + + ))} + + + + + + + {qnhistState.selectedQnHistory.submissionStatus} + + + + + {convertDateString(qnhistState.selectedQnHistory.dateAttempted)} + + + + + {`${qnhistState.selectedQnHistory.timeTaken} mins`} + + + + navigate(`/profile/${partnerId}`)} + > + {"Go to partner profile"} + + + + +
+
+ } + + ({ flex: 1, marginRight: theme.spacing(2) })}> + {qnState.selectedQuestion + ? + : + } + + ({ flex: 1, marginLeft: theme.spacing(2) })}> + + Code editor + + + +
+ ); +}; + +export default QuestionHistoryDetail; diff --git a/frontend/src/reducers/qnHistoryReducer.ts b/frontend/src/reducers/qnHistoryReducer.ts index 61ca19340c..9d74707b03 100644 --- a/frontend/src/reducers/qnHistoryReducer.ts +++ b/frontend/src/reducers/qnHistoryReducer.ts @@ -11,6 +11,7 @@ type QnHistoryDetail = { dateAttempted: string; timeTaken: number; code: string; + language: string; }; type QnHistoryList = { @@ -59,7 +60,8 @@ const isQnHistory = (qnHistory: any): qnHistory is QnHistoryDetail => { "[object Date]" && !isNaN(new Date(qnHistory.dateAttempted).getTime()) && typeof qnHistory.timeTaken === "number" && - isString(qnHistory.code) + isString(qnHistory.code) && + isString(qnHistory.language) ); }; @@ -93,24 +95,16 @@ export const createQnHistory = async ( qnHistory: Omit, dispatch: Dispatch ): Promise => { - const accessToken = localStorage.getItem("token"); return qnHistoryClient - .post( - "/", - { - userIds: qnHistory.userIds, - questionId: qnHistory.questionId, - title: qnHistory.title, - submissionStatus: qnHistory.submissionStatus, - dateAttempted: qnHistory.dateAttempted, - timeTaken: qnHistory.timeTaken, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - } - ) + .post("/", { + userIds: qnHistory.userIds, + questionId: qnHistory.questionId, + title: qnHistory.title, + submissionStatus: qnHistory.submissionStatus, + dateAttempted: qnHistory.dateAttempted, + timeTaken: qnHistory.timeTaken, + language: qnHistory.language, + }) .then((res) => { dispatch({ type: QnHistoryActionTypes.CREATE_QNHIST, @@ -178,25 +172,19 @@ export const getQnHistoryById = ( export const updateQnHistoryById = async ( qnHistoryId: string, - qnHistory: Omit, + qnHistory: Omit< + QnHistoryDetail, + "id" | "userIds" | "questionId" | "title" | "language" + >, dispatch: Dispatch ): Promise => { - const accessToken = localStorage.getItem("token"); return qnHistoryClient - .put( - `/${qnHistoryId}`, - { - submissionStatus: qnHistory.submissionStatus, - dateAttempted: qnHistory.dateAttempted, - timeTaken: qnHistory.timeTaken, - code: qnHistory.code, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - } - ) + .put(`/${qnHistoryId}`, { + submissionStatus: qnHistory.submissionStatus, + dateAttempted: qnHistory.dateAttempted, + timeTaken: qnHistory.timeTaken, + code: qnHistory.code, + }) .then((res) => { dispatch({ type: QnHistoryActionTypes.UPDATE_QNHIST, @@ -215,12 +203,7 @@ export const updateQnHistoryById = async ( export const deleteQuestionById = async (qnHistoryId: string) => { try { - const accessToken = localStorage.getItem("token"); - await qnHistoryClient.delete(`/${qnHistoryId}`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); + await qnHistoryClient.delete(`/${qnHistoryId}`); return true; } catch { return false; From 287657f2c59f06cb7d24712fa12d1d69a47af714 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:16:48 +0800 Subject: [PATCH 057/192] Add title to qnhistdetails page --- frontend/src/pages/QuestionHistoryDetail/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/QuestionHistoryDetail/index.tsx b/frontend/src/pages/QuestionHistoryDetail/index.tsx index 24076ddefd..e599c9eda4 100644 --- a/frontend/src/pages/QuestionHistoryDetail/index.tsx +++ b/frontend/src/pages/QuestionHistoryDetail/index.tsx @@ -75,6 +75,7 @@ const QuestionHistoryDetail: React.FC = () => { navigate(-1)}> + Latest submission details { user && qnhistState.selectedQnHistory && Date: Thu, 31 Oct 2024 23:15:43 +0800 Subject: [PATCH 058/192] Allow code editor to be read only --- .../src/handlers/websocketHandler.ts | 80 ++++++++++ backend/collab-service/src/server.ts | 50 ------ frontend/src/components/CodeEditor/index.tsx | 48 ++---- frontend/src/pages/CollabSandbox/index.tsx | 5 +- frontend/src/utils/collabCursor.ts | 6 +- frontend/src/utils/collabSocket.ts | 146 ++++++++++-------- 6 files changed, 182 insertions(+), 153 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 5a806a70d4..ba760d5cc7 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -1,6 +1,8 @@ import { Socket } from "socket.io"; import { io } from "../server"; import redisClient from "../config/redis"; +import { ChangeSet, Text } from "@codemirror/state"; +import { rebaseUpdates, Update } from "@codemirror/collab"; enum CollabEvents { // Receive @@ -9,6 +11,10 @@ enum CollabEvents { LEAVE = "leave", DISCONNECT = "disconnect", + PUSH_UPDATES = "push_updates", + PULL_UPDATES = "pull_updates", + GET_DOCUMENT = "get_document", + // Send ROOM_FULL = "room_full", CONNECTED = "connected", @@ -16,6 +22,9 @@ enum CollabEvents { CODE_CHANGE = "code_change", PARTNER_LEFT = "partner_left", PARTNER_DISCONNECTED = "partner_disconnected", + + PULL_UPDATES_RESPONSE = "pull_updates_response", + GET_DOCUMENT_RESPONSE = "get_document_response", } const EXPIRY_TIME = 3600; @@ -69,4 +78,75 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { socket.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED); } }); + + handleCodeEditorEvents(socket); +}; + +/* Code Editor Events */ +// Adapted from https://codemirror.net/examples/collab/ and https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets + +let updates: Update[] = []; // updates.length = current version +let doc = Text.of(["Start document"]); +let pendingPullUpdatesRequests: ((updates: Update[]) => void)[] = []; + +const handleCodeEditorEvents = (socket: Socket) => { + socket.on(CollabEvents.PULL_UPDATES, (version: number) => { + if (version < updates.length) { + // send the new updates + socket.emit( + CollabEvents.PULL_UPDATES_RESPONSE, + JSON.stringify(updates.slice(version)) + ); + } else { + // wait until there are new updates to send + pendingPullUpdatesRequests.push((updates) => { + socket.emit( + CollabEvents.PULL_UPDATES_RESPONSE, + JSON.stringify(updates.slice(version)) + ); + }); + } + }); + + // received new updates, notify any pending pullUpdates requests + socket.on( + CollabEvents.PUSH_UPDATES, + (version: number, newUpdates: string, callback: () => void) => { + let docUpdates = JSON.parse(newUpdates) as readonly Update[]; + + try { + // If the given version is the latest version, apply the new updates. + // Else, rebase updates first. + if (version != updates.length) { + docUpdates = rebaseUpdates(docUpdates, updates.slice(version)); + } + + for (const update of docUpdates) { + const changes = ChangeSet.fromJSON(update.changes); + updates.push({ + clientID: update.clientID, + changes: changes, + effects: update.effects, + }); + doc = changes.apply(doc); + } + callback(); + + while (pendingPullUpdatesRequests.length) { + pendingPullUpdatesRequests.pop()!(updates); + } + } catch (error) { + console.error(error); + callback(); + } + } + ); + + socket.on(CollabEvents.GET_DOCUMENT, () => { + socket.emit( + CollabEvents.GET_DOCUMENT_RESPONSE, + updates.length, + doc.toString() + ); + }); }; diff --git a/backend/collab-service/src/server.ts b/backend/collab-service/src/server.ts index b00a479e02..c1d11c7333 100644 --- a/backend/collab-service/src/server.ts +++ b/backend/collab-service/src/server.ts @@ -3,12 +3,6 @@ import app, { allowedOrigins } from "./app.ts"; import { handleWebsocketCollabEvents } from "./handlers/websocketHandler.ts"; import { Server, Socket } from "socket.io"; import { connectRedis } from "./config/redis.ts"; -import { ChangeSet, Text } from "@codemirror/state"; -import { Update } from "@codemirror/collab"; - -let updates: Update[] = []; -let doc = Text.of(["Start document"]); -let pending: ((value: any) => void)[] = []; const server = http.createServer(app); export const io = new Server(server, { @@ -21,50 +15,6 @@ export const io = new Server(server, { io.on("connection", (socket: Socket) => { handleWebsocketCollabEvents(socket); - - socket.on("pullUpdates", (version: number) => { - if (version < updates.length) { - socket.emit("pullUpdateResponse", JSON.stringify(updates.slice(version))); - } else { - pending.push((updates) => { - socket.emit( - "pullUpdateResponse", - JSON.stringify(updates.slice(version)) - ); - }); - } - }); - - socket.on("pushUpdates", (version, docUpdates) => { - docUpdates = JSON.parse(docUpdates); - - try { - if (version != updates.length) { - socket.emit("pushUpdateResponse", false); - } else { - for (let update of docUpdates) { - let changes = ChangeSet.fromJSON(update.changes); - updates.push({ - changes, - clientID: update.clientID, - effects: update.effects, // cursor - }); - doc = changes.apply(doc); - } - socket.emit("pushUpdateResponse", true); - - while (pending.length) { - pending.pop()!(updates); - } - } - } catch (error) { - console.error(error); - } - }); - - socket.on("getDocument", () => { - socket.emit("getDocumentResponse", updates.length, doc.toString()); - }); }); const PORT = process.env.SERVICE_PORT || 3003; diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index ce8c54ca0c..0140f189f2 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -1,30 +1,32 @@ import CodeMirror from "@uiw/react-codemirror"; import { langs } from "@uiw/codemirror-extensions-langs"; import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; +import { EditorView } from "@codemirror/view"; +import { EditorState } from "@codemirror/state"; import { useEffect, useState } from "react"; import { - collabSocket, getDocument, peerExtension, + removeListeners, } from "../../utils/collabSocket"; import Loader from "../Loader"; import { cursorExtension } from "../../utils/collabCursor"; interface CodeEditorProps { + uid: string; username: string; + isReadOnly?: boolean; } -type EditorState = { - connected: boolean; +type CodeEditorState = { version: number | null; doc: string | null; }; const CodeEditor: React.FC = (props) => { - const { username } = props; + const { uid, username, isReadOnly = false } = props; - const [editorState, setEditorState] = useState({ - connected: false, + const [codeEditorState, setCodeEditorState] = useState({ version: null, doc: null, }); @@ -33,24 +35,9 @@ const CodeEditor: React.FC = (props) => { const fetchDocument = async () => { try { const { version, doc } = await getDocument(); - setEditorState((prevState) => ({ - ...prevState, + setCodeEditorState({ version: version, doc: doc.toString(), - })); - - collabSocket.on("connect", () => { - setEditorState((prevState) => ({ - ...prevState, - connected: true, - })); - }); - - collabSocket.on("disconnect", () => { - setEditorState((prevState) => ({ - ...prevState, - connected: false, - })); }); } catch (error) { console.error("Error fetching document: ", error); @@ -59,16 +46,10 @@ const CodeEditor: React.FC = (props) => { fetchDocument(); - return () => { - collabSocket.off("connect"); - collabSocket.off("disconnect"); - collabSocket.off("pullUpdateResponse"); - collabSocket.off("pushUpdateResponse"); - collabSocket.off("getDocumentResponse"); - }; + return () => removeListeners(); }, []); - if (editorState.version === null || editorState.doc === null) { + if (codeEditorState.version === null || codeEditorState.doc === null) { return ; } @@ -81,11 +62,12 @@ const CodeEditor: React.FC = (props) => { extensions={[ basicSetup(), langs.c(), - // peerExtension(editorState.version), - peerExtension(editorState.version, username), + peerExtension(codeEditorState.version, uid), cursorExtension(username), + EditorView.editable.of(!isReadOnly), + EditorState.readOnly.of(isReadOnly), ]} - value={editorState.doc} + value={codeEditorState.doc} /> ); }; diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 1f61e755fb..b858392371 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -100,9 +100,6 @@ const CollabSandbox: React.FC = () => { return ( - {/* - Successfully matched! - */} { sx={{ display: "flex", flexDirection: "column", height: "100%" }} > - + Test cases and chat tabs diff --git a/frontend/src/utils/collabCursor.ts b/frontend/src/utils/collabCursor.ts index 81aadaa956..f3621a4079 100644 --- a/frontend/src/utils/collabCursor.ts +++ b/frontend/src/utils/collabCursor.ts @@ -6,6 +6,8 @@ import { } from "@codemirror/view"; import { StateField, StateEffect } from "@codemirror/state"; +// Adapted from https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets + export interface Cursor { id: string; from: number; @@ -191,7 +193,7 @@ const cursorBaseTheme = EditorView.baseTheme({ }, }); -export const cursorExtension = (id: string = "") => { +export const cursorExtension = (username: string) => { return [ cursorField, cursorBaseTheme, @@ -199,7 +201,7 @@ export const cursorExtension = (id: string = "") => { update.transactions.forEach((e) => { if (e.selection) { const cursor: Cursor = { - id, + id: username, from: e.selection.ranges[0].from, to: e.selection.ranges[0].to, }; diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 8d366f0516..78a23597a0 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -10,61 +10,69 @@ import { import { io } from "socket.io-client"; import { addCursor, Cursor, removeCursor } from "./collabCursor"; -export const collabSocket = io("http://localhost:3003"); +// Adapted from https://codemirror.net/examples/collab/ and https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets + +enum CollabEvents { + // Send + PUSH_UPDATES = "push_updates", + PULL_UPDATES = "pull_updates", + GET_DOCUMENT = "get_document", + + // Receive + PULL_UPDATES_RESPONSE = "pull_updates_response", + GET_DOCUMENT_RESPONSE = "get_document_response", +} + +const collabSocket = io("http://localhost:3003"); const pushUpdates = ( version: number, fullUpdates: readonly Update[] -): Promise => { - const updates = fullUpdates.map((u) => ({ - clientID: u.clientID, - changes: u.changes.toJSON(), - effects: u.effects, // cursor +): Promise => { + const updates = fullUpdates.map((update) => ({ + clientID: update.clientID, // client who made the update + changes: update.changes.toJSON(), // document updates + effects: update.effects, // cursor updates })); - return new Promise(function (resolve) { - collabSocket.emit("pushUpdates", version, JSON.stringify(updates)); - - collabSocket.once("pushUpdateResponse", (status: boolean) => { - resolve(status); - }); + return new Promise((resolve) => { + collabSocket.emit( + CollabEvents.PUSH_UPDATES, + version, + JSON.stringify(updates), + () => resolve() + ); }); }; const pullUpdates = (version: number): Promise => { - return new Promise(function (resolve) { - collabSocket.emit("pullUpdates", version); + return new Promise((resolve) => { + collabSocket.emit(CollabEvents.PULL_UPDATES, version); - collabSocket.once("pullUpdateResponse", (updates: any) => { + collabSocket.once(CollabEvents.PULL_UPDATES_RESPONSE, (updates: string) => { resolve(JSON.parse(updates)); }); - }).then((updates: any) => - // updates.map((u: any) => ({ - // changes: ChangeSet.fromJSON(u.changes), - // clientID: u.clientID, - // })) - - updates.map((u: any) => { + }).then((updates) => + updates.map((update) => { const effects: StateEffect[] = []; - if (u.effects?.length) { - u.effects.forEach((effect: StateEffect) => { - if (effect.value?.id && effect.value?.from) { - const cursor: Cursor = { - id: effect.value.id, - from: effect.value.from, - to: effect.value.to, - }; - effects.push(addCursor.of(cursor)); - } else if (effect.value?.id) { - const cursorId = effect.value.id; - effects.push(removeCursor.of(cursorId)); - } - }); - } + + update.effects?.forEach((effect) => { + if (effect.value?.id && effect.value?.from) { + const cursor: Cursor = { + id: effect.value.id, + from: effect.value.from, + to: effect.value.to, + }; + effects.push(addCursor.of(cursor)); + } else if (effect.value?.id) { + const cursorId = effect.value.id; + effects.push(removeCursor.of(cursorId)); + } + }); return { - changes: ChangeSet.fromJSON(u.changes), - clientID: u.clientID, + clientID: update.clientID, + changes: ChangeSet.fromJSON(update.changes), effects: effects, }; }) @@ -72,23 +80,27 @@ const pullUpdates = (version: number): Promise => { }; export const getDocument = (): Promise<{ version: number; doc: Text }> => { - return new Promise(function (resolve) { - collabSocket.emit("getDocument"); - - collabSocket.once("getDocumentResponse", (version: number, doc: string) => { - resolve({ - version: version, - doc: Text.of(doc.split("\n")), - }); - }); + return new Promise((resolve) => { + collabSocket.emit(CollabEvents.GET_DOCUMENT); + + collabSocket.once( + CollabEvents.GET_DOCUMENT_RESPONSE, + (version: number, doc: string) => { + resolve({ + version: version, + doc: Text.of(doc.split("\n")), + }); + } + ); }); }; -export const peerExtension = (startVersion: number, id?: string) => { +// handles push and pull updates +export const peerExtension = (startVersion: number, uid: string) => { const plugin = ViewPlugin.fromClass( class { - private pushing = false; - private done = false; + private pushingUpdates = false; // to ensure only one running push request + private pullUpdates = true; constructor(private view: EditorView) { this.pull(); @@ -96,48 +108,54 @@ export const peerExtension = (startVersion: number, id?: string) => { update(update: ViewUpdate) { if (update.docChanged || update.transactions.length) { - // cursor - // if (update.docChanged) { this.push(); } } async push() { const updates = sendableUpdates(this.view.state); - if (this.pushing || !updates.length) { + if (this.pushingUpdates || !updates.length) { return; } - this.pushing = true; + this.pushingUpdates = true; const version = getSyncedVersion(this.view.state); await pushUpdates(version, updates); - this.pushing = false; + this.pushingUpdates = false; + + // check if there are still updates to push (failed / new updates) if (sendableUpdates(this.view.state).length) { setTimeout(() => this.push(), 100); } } async pull() { - while (!this.done) { + while (this.pullUpdates) { const version = getSyncedVersion(this.view.state); - const updates = await pullUpdates(version); + const updates = await pullUpdates(version); // returns only if there are updates this.view.dispatch(receiveUpdates(this.view.state, updates)); } } destroy() { - this.done = true; + this.pullUpdates = false; } } ); - // return [collab({ startVersion }), plugin]; return [ collab({ - startVersion, - clientID: id, - sharedEffects: (tr) => - tr.effects.filter((e) => e.is(addCursor) || e.is(removeCursor)), + startVersion: startVersion, + clientID: uid, + sharedEffects: (transaction) => + transaction.effects.filter( + (effect) => effect.is(addCursor) || effect.is(removeCursor) + ), }), plugin, ]; }; + +export const removeListeners = () => { + collabSocket.off(CollabEvents.PULL_UPDATES_RESPONSE); + collabSocket.off(CollabEvents.GET_DOCUMENT_RESPONSE); +}; From fc2f3f8125d96297e68a021b2320b3dc56f2e9e5 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Fri, 1 Nov 2024 00:25:10 +0800 Subject: [PATCH 059/192] Implement BE for storing testcases and integrate with FE --- backend/question-service/src/config/multer.ts | 6 +- .../src/controllers/questionController.ts | 64 ++++++++++++++++++- .../question-service/src/models/Question.ts | 23 +++++-- .../src/routes/questionRoutes.ts | 3 + backend/question-service/src/utils/utils.ts | 3 +- .../QuestionFileContainer/index.tsx | 18 ------ frontend/src/pages/NewQuestion/index.tsx | 45 +++++++------ frontend/src/reducers/questionReducer.ts | 57 +++++++++++++++++ 8 files changed, 173 insertions(+), 46 deletions(-) diff --git a/backend/question-service/src/config/multer.ts b/backend/question-service/src/config/multer.ts index 40e5e1919a..c3ec6c1e95 100644 --- a/backend/question-service/src/config/multer.ts +++ b/backend/question-service/src/config/multer.ts @@ -2,5 +2,9 @@ import multer from "multer"; const storage = multer.memoryStorage(); const upload = multer({ storage }).array("images[]"); +const uploadTestcaseFiles = multer({ storage }).fields([ + { name: "testcaseInputFile", maxCount: 1 }, + { name: "testcaseOutputFile", maxCount: 1 }, +]); -export { upload }; +export { upload, uploadTestcaseFiles }; diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index ccce137ba6..ac0e450560 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -20,10 +20,12 @@ import { MONGO_OBJ_ID_MALFORMED_MESSAGE, } from "../utils/constants.ts"; -import { upload } from "../config/multer.ts"; +import { upload, uploadTestcaseFiles } from "../config/multer.ts"; import { uploadFileToFirebase } from "../utils/utils"; import { QnListSearchFilterParams, RandomQnCriteria } from "../utils/types.ts"; +const FIREBASE_TESTCASE_FILES_FOLDER_NAME = "testcaseFiles/"; + export const createQuestion = async ( req: Request, res: Response, @@ -34,6 +36,9 @@ export const createQuestion = async ( description, complexity, category, + testcases, + testcaseInputFileUrl, + testcaseOutputFileUrl, pythonTemplate, javaTemplate, cTemplate, @@ -59,6 +64,9 @@ export const createQuestion = async ( description, complexity, category, + testcases, + testcaseInputFileUrl, + testcaseOutputFileUrl, }); await newQuestion.save(); @@ -77,6 +85,7 @@ export const createQuestion = async ( question: formatQuestionIndivResponse(newQuestion, newQuestionTemplate), }); } catch (error) { + console.log(error); res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); } }; @@ -110,6 +119,59 @@ export const createImageLink = async ( }); }; +export const createFileLink = async ( + req: Request, + res: Response, +): Promise => { + uploadTestcaseFiles(req, res, async (err) => { + if (err) { + return res.status(500).json({ + message: "Failed to upload testcase files", + error: err.message, + }); + } + + const tcFiles = req.files as { + testcaseInputFile?: Express.Multer.File[]; + testcaseOutputFile?: Express.Multer.File[]; + }; + + if (!tcFiles || !tcFiles.testcaseInputFile || !tcFiles.testcaseOutputFile) { + return res + .status(400) + .json({ message: "Missing one or both testcase file(s)" }); + } + + try { + const testcaseInputFile = tcFiles + .testcaseInputFile[0] as Express.Multer.File; + const testcaseOutputFile = tcFiles + .testcaseOutputFile[0] as Express.Multer.File; + + const [tcInputFileUrl, tcOutputFileUrl] = await Promise.all([ + uploadFileToFirebase( + testcaseInputFile, + FIREBASE_TESTCASE_FILES_FOLDER_NAME, + ), + uploadFileToFirebase( + testcaseOutputFile, + FIREBASE_TESTCASE_FILES_FOLDER_NAME, + ), + ]); + + return res.status(200).json({ + message: "Files uploaded successfully", + urls: { + testcaseInputFileUrl: tcInputFileUrl, + testcaseOutputFileUrl: tcOutputFileUrl, + }, + }); + } catch (error) { + return res.status(500).json({ message: "Server error", error }); + } + }); +}; + export const updateQuestion = async ( req: Request, res: Response, diff --git a/backend/question-service/src/models/Question.ts b/backend/question-service/src/models/Question.ts index 40e86f6fb3..7dcebb77a3 100644 --- a/backend/question-service/src/models/Question.ts +++ b/backend/question-service/src/models/Question.ts @@ -1,14 +1,29 @@ import mongoose, { Schema, Document } from "mongoose"; +export interface ITestcase { + id: string; + input: string; + expectedOutput: string; +} + export interface IQuestion extends Document { title: string; description: string; complexity: string; category: string[]; + testcases: ITestcase[]; + testcaseInputFileUrl: string; + testcaseOutputFileUrl: string; createdAt: Date; updatedAt: Date; } +const testcaseSchema: Schema = new mongoose.Schema({ + id: { type: String, required: true }, + input: { type: String, required: true }, + expectedOutput: { type: String, required: true }, +}); + const questionSchema: Schema = new mongoose.Schema( { title: { type: String, required: true }, @@ -18,10 +33,10 @@ const questionSchema: Schema = new mongoose.Schema( enum: ["Easy", "Medium", "Hard"], required: true, }, - category: { - type: [String], - required: true, - }, + category: { type: [String], required: true }, + testcases: { type: [testcaseSchema], required: true }, + testcaseInputFileUrl: { type: String, required: true }, + testcaseOutputFileUrl: { type: String, required: true }, }, { timestamps: true }, ); diff --git a/backend/question-service/src/routes/questionRoutes.ts b/backend/question-service/src/routes/questionRoutes.ts index 318f168620..b2d5f1c949 100644 --- a/backend/question-service/src/routes/questionRoutes.ts +++ b/backend/question-service/src/routes/questionRoutes.ts @@ -8,6 +8,7 @@ import { readQuestionIndiv, readCategories, readRandomQuestion, + createFileLink, } from "../controllers/questionController.ts"; import { verifyAdminToken } from "../middlewares/basicAccessControl.ts"; @@ -17,6 +18,8 @@ router.post("/", verifyAdminToken, createQuestion); router.post("/images", verifyAdminToken, createImageLink); +router.post("/tcfiles", verifyAdminToken, createFileLink); + router.put("/:id", verifyAdminToken, updateQuestion); router.get("/categories", readCategories); diff --git a/backend/question-service/src/utils/utils.ts b/backend/question-service/src/utils/utils.ts index 8e93776d23..4bccc53823 100644 --- a/backend/question-service/src/utils/utils.ts +++ b/backend/question-service/src/utils/utils.ts @@ -21,9 +21,10 @@ export const checkIsExistingQuestion = async ( export const uploadFileToFirebase = async ( file: Express.Multer.File, + folderName: string = "", ): Promise => { return new Promise((resolve, reject) => { - const fileName = uuidv4(); + const fileName = folderName + uuidv4(); const ref = bucket.file(fileName); const blobStream = ref.createWriteStream({ diff --git a/frontend/src/components/QuestionFileContainer/index.tsx b/frontend/src/components/QuestionFileContainer/index.tsx index b045c5f85e..8374dc18ff 100644 --- a/frontend/src/components/QuestionFileContainer/index.tsx +++ b/frontend/src/components/QuestionFileContainer/index.tsx @@ -43,24 +43,6 @@ const QuestionFileContainer: React.FC = ({ } setFile(file); - console.log(event); - - // const formData = new FormData(); - - // if (formData.getAll("images[]").length === 0) { - // return; - // } - - // createImageUrls(formData).then((res) => { - // if (res) { - // for (const imageUrl of res.imageUrls) { - // setUploadedImagesUrl((prev) => [...prev, imageUrl]); - // } - // toast.success(SUCCESS_FILE_UPLOAD); - // } else { - // toast.error(FAILED_FILE_UPLOAD); - // } - // }); }; return ( diff --git a/frontend/src/pages/NewQuestion/index.tsx b/frontend/src/pages/NewQuestion/index.tsx index f1b5668b73..2b000fe350 100644 --- a/frontend/src/pages/NewQuestion/index.tsx +++ b/frontend/src/pages/NewQuestion/index.tsx @@ -95,27 +95,30 @@ const NewQuestion = () => { return; } - // const result = await createQuestion( - // { - // title, - // description: markdownText, - // complexity: selectedComplexity, - // categories: selectedCategories, - // pythonTemplate: codeTemplates.python, - // javaTemplate: codeTemplates.java, - // cTemplate: codeTemplates.c, - // }, - // dispatch - // ); - - // if (result) { - // navigate("/questions"); - // toast.success(SUCCESS_QUESTION_CREATE); - // } else { - // toast.error(state.selectedQuestionError || FAILED_QUESTION_CREATE); - // } - - console.log("successfully submit"); + const result = await createQuestion( + { + title, + description: markdownText, + complexity: selectedComplexity, + categories: selectedCategories, + testcases: testCases, + pythonTemplate: codeTemplates.python, + javaTemplate: codeTemplates.java, + cTemplate: codeTemplates.c, + }, + { + testcaseInputFile: testcaseInputFile, + testcaseOutputFile: testcaseOutputFile, + }, + dispatch + ); + + if (result) { + navigate("/questions"); + toast.success(SUCCESS_QUESTION_CREATE); + } else { + toast.error(state.selectedQuestionError || FAILED_QUESTION_CREATE); + } }; return ( diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts index b7e57a7063..1eb959d46b 100644 --- a/frontend/src/reducers/questionReducer.ts +++ b/frontend/src/reducers/questionReducer.ts @@ -2,12 +2,24 @@ import { Dispatch } from "react"; import { questionClient } from "../utils/api"; import { isString, isStringArray } from "../utils/typeChecker"; +type TestcaseFiles = { + testcaseInputFile: File; + testcaseOutputFile: File; +}; + +type Testcases = { + id: string; + input: string; + expectedOutput: string; +}; + type QuestionDetail = { id: string; title: string; description: string; complexity: string; categories: Array; + testcases: Array; pythonTemplate: string; javaTemplate: string; cTemplate: string; @@ -93,10 +105,50 @@ export const initialState: QuestionsState = { selectedQuestionError: null, }; +export const uploadTestcaseFiles = async ( + data: TestcaseFiles +): Promise<{ + message: string; + urls: { + testcaseInputFileUrl: string; + testcaseOutputFileUrl: string; + }; +} | null> => { + const formData = new FormData(); + formData.append("testcaseInputFile", data.testcaseInputFile); + formData.append("testcaseOutputFile", data.testcaseOutputFile); + + try { + const accessToken = localStorage.getItem("token"); + const res = await questionClient.post("/tcfiles", formData, { + headers: { + "Content-Type": "multipart/form-data", + Authorization: `Bearer ${accessToken}`, + }, + }); + return res.data; + } catch { + return null; + } +}; + export const createQuestion = async ( question: Omit, + testcaseFiles: TestcaseFiles, dispatch: Dispatch ): Promise => { + const uploadResult = await uploadTestcaseFiles(testcaseFiles); + + if (!uploadResult) { + dispatch({ + type: QuestionActionTypes.ERROR_CREATING_QUESTION, + payload: "Failed to upload test case files.", + }); + return false; + } + + const { testcaseInputFileUrl, testcaseOutputFileUrl } = uploadResult.urls; + const accessToken = localStorage.getItem("token"); return questionClient .post( @@ -106,6 +158,9 @@ export const createQuestion = async ( description: question.description, complexity: question.complexity, category: question.categories, + testcases: question.testcases, + testcaseInputFileUrl, + testcaseOutputFileUrl, pythonTemplate: question.pythonTemplate, cTemplate: question.cTemplate, javaTemplate: question.javaTemplate, @@ -124,6 +179,8 @@ export const createQuestion = async ( return true; }) .catch((err) => { + console.log(err.response?.data.message || err.message); + dispatch({ type: QuestionActionTypes.ERROR_CREATING_QUESTION, payload: err.response?.data.message || err.message, From 129add10e123119d0f3a6cda00f4903c73c72011 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Fri, 1 Nov 2024 01:04:03 +0800 Subject: [PATCH 060/192] Differentiate collab cursor by uid --- frontend/src/components/CodeEditor/index.tsx | 2 +- frontend/src/utils/collabCursor.ts | 83 +++++++++----------- frontend/src/utils/collabSocket.ts | 16 ++-- 3 files changed, 46 insertions(+), 55 deletions(-) diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 0140f189f2..553b1bb30b 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -63,7 +63,7 @@ const CodeEditor: React.FC = (props) => { basicSetup(), langs.c(), peerExtension(codeEditorState.version, uid), - cursorExtension(username), + cursorExtension(uid, username), EditorView.editable.of(!isReadOnly), EditorState.readOnly.of(isReadOnly), ]} diff --git a/frontend/src/utils/collabCursor.ts b/frontend/src/utils/collabCursor.ts index f3621a4079..c0391a9d4c 100644 --- a/frontend/src/utils/collabCursor.ts +++ b/frontend/src/utils/collabCursor.ts @@ -9,15 +9,12 @@ import { StateField, StateEffect } from "@codemirror/state"; // Adapted from https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets export interface Cursor { - id: string; + uid: string; + username: string; from: number; to: number; } -export interface Cursors { - cursors: Cursor[]; -} - class TooltipWidget extends WidgetType { private name = "John"; private suffix = ""; @@ -49,55 +46,51 @@ class TooltipWidget extends WidgetType { } } -export const addCursor = StateEffect.define(); -export const removeCursor = StateEffect.define(); +export const updateCursor = StateEffect.define(); -const cursorsItems = new Map(); +const cursors = new Map(); -const cursorField = StateField.define({ - create() { - return Decoration.none; - }, - update(cursors, tr) { - let cursorTransacions = cursors.map(tr.changes); - for (const e of tr.effects) - if (e.is(addCursor)) { +const cursorStateField = StateField.define({ + create: () => Decoration.none, + update: (prevCursorState, transaction) => { + let cursorTransactions = prevCursorState.map(transaction.changes); + for (const effect of transaction.effects) { + if (effect.is(updateCursor)) { const addUpdates = []; - if (!cursorsItems.has(e.value.id)) - cursorsItems.set(e.value.id, cursorsItems.size); - - if (e.value.from !== e.value.to) { + if (!cursors.has(effect.value.uid)) { + cursors.set(effect.value.uid, cursors.size); + } + if (effect.value.from !== effect.value.to) { + // highlight selected text addUpdates.push( Decoration.mark({ - class: `cm-highlight-${(cursorsItems.get(e.value.id)! % 8) + 1}`, - id: e.value.id, - }).range(e.value.from, e.value.to) + class: `cm-highlight-${(cursors.get(effect.value.uid)! % 8) + 1}`, + uid: effect.value.uid, + }).range(effect.value.from, effect.value.to) ); } addUpdates.push( Decoration.widget({ widget: new TooltipWidget( - e.value.id, - cursorsItems.get(e.value.id)! + effect.value.username, + cursors.get(effect.value.uid)! ), block: false, - id: e.value.id, - }).range(e.value.to, e.value.to) + uid: effect.value.uid, + }).range(effect.value.to, effect.value.to) ); - cursorTransacions = cursorTransacions.update({ + // ensure only the latest cursor position and/or selection is displayed + cursorTransactions = cursorTransactions.update({ add: addUpdates, - filter: (_from, _to, value) => { - if (value?.spec?.id === e.value.id) return false; - return true; - }, + filter: (_from, _to, value) => value?.spec?.uid !== effect.value.uid, }); } - - return cursorTransacions; + } + return cursorTransactions; }, - provide: (f) => EditorView.decorations.from(f), + provide: (field) => EditorView.decorations.from(field), }); const cursorBaseTheme = EditorView.baseTheme({ @@ -193,21 +186,23 @@ const cursorBaseTheme = EditorView.baseTheme({ }, }); -export const cursorExtension = (username: string) => { +export const cursorExtension = (uid: string, username: string) => { return [ - cursorField, - cursorBaseTheme, + cursorStateField, // handles cursor positions and highlights + cursorBaseTheme, // provides cursor styling + // detects cursor updates EditorView.updateListener.of((update) => { - update.transactions.forEach((e) => { - if (e.selection) { + update.transactions.forEach((transaction) => { + if (transaction.selection) { const cursor: Cursor = { - id: username, - from: e.selection.ranges[0].from, - to: e.selection.ranges[0].to, + uid: uid, + username: username, + from: transaction.selection.ranges[0].from, + to: transaction.selection.ranges[0].to, }; update.view.dispatch({ - effects: addCursor.of(cursor), + effects: updateCursor.of(cursor), }); } }); diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 78a23597a0..ced6e6551a 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -8,7 +8,7 @@ import { getSyncedVersion, } from "@codemirror/collab"; import { io } from "socket.io-client"; -import { addCursor, Cursor, removeCursor } from "./collabCursor"; +import { updateCursor, Cursor } from "./collabCursor"; // Adapted from https://codemirror.net/examples/collab/ and https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets @@ -57,16 +57,14 @@ const pullUpdates = (version: number): Promise => { const effects: StateEffect[] = []; update.effects?.forEach((effect) => { - if (effect.value?.id && effect.value?.from) { + if (effect.value?.uid && effect.value?.from) { const cursor: Cursor = { - id: effect.value.id, + uid: effect.value.uid, + username: effect.value.username, from: effect.value.from, to: effect.value.to, }; - effects.push(addCursor.of(cursor)); - } else if (effect.value?.id) { - const cursorId = effect.value.id; - effects.push(removeCursor.of(cursorId)); + effects.push(updateCursor.of(cursor)); } }); @@ -147,9 +145,7 @@ export const peerExtension = (startVersion: number, uid: string) => { startVersion: startVersion, clientID: uid, sharedEffects: (transaction) => - transaction.effects.filter( - (effect) => effect.is(addCursor) || effect.is(removeCursor) - ), + transaction.effects.filter((effect) => effect.is(updateCursor)), }), plugin, ]; From 8302d1d2acb1ad840575a0785773b2315219acab Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Fri, 1 Nov 2024 01:54:23 +0800 Subject: [PATCH 061/192] change qnhistoryservice port number --- README.md | 2 +- backend/matching-service/src/utils/api.ts | 2 +- backend/qn-history-service/.env.sample | 2 +- backend/qn-history-service/Dockerfile | 2 +- backend/qn-history-service/README.md | 4 ++-- backend/qn-history-service/server.ts | 2 +- docker-compose-test.yml | 2 +- docker-compose.yml | 2 +- frontend/src/utils/api.ts | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2a4964045e..b7f8659cc5 100644 --- a/README.md +++ b/README.md @@ -31,5 +31,5 @@ docker-compose down - Matching Service: http://localhost:3002 - Collab Service: http://localhost:3003 - Code Execution Service: http://localhost:3004 -- Question History Service: http://localhost:3005 +- Question History Service: http://localhost:3006 - Frontend: http://localhost:5173 diff --git a/backend/matching-service/src/utils/api.ts b/backend/matching-service/src/utils/api.ts index 36e5dfd617..1b088f4c05 100644 --- a/backend/matching-service/src/utils/api.ts +++ b/backend/matching-service/src/utils/api.ts @@ -6,7 +6,7 @@ const QUESTION_SERVICE_URL = const QN_HISTORY_SERVICE_URL = process.env.QN_HISTORY_SERVICE_URL || - "http://qn-history-service:3005/api/qnhistories"; + "http://qn-history-service:3006/api/qnhistories"; export const questionService = axios.create({ baseURL: QUESTION_SERVICE_URL, diff --git a/backend/qn-history-service/.env.sample b/backend/qn-history-service/.env.sample index fa12352eca..0583d0a3e4 100644 --- a/backend/qn-history-service/.env.sample +++ b/backend/qn-history-service/.env.sample @@ -1,5 +1,5 @@ NODE_ENV=development -SERVICE_PORT=3005 +SERVICE_PORT=3006 ORIGINS=http://localhost:5173,http://127.0.0.1:5173 diff --git a/backend/qn-history-service/Dockerfile b/backend/qn-history-service/Dockerfile index 686853d5f5..269bc6c1d7 100644 --- a/backend/qn-history-service/Dockerfile +++ b/backend/qn-history-service/Dockerfile @@ -8,6 +8,6 @@ RUN npm ci COPY . . -EXPOSE 3005 +EXPOSE 3006 CMD ["npm", "run", "dev"] diff --git a/backend/qn-history-service/README.md b/backend/qn-history-service/README.md index e1c3c7a644..2a15faaea3 100644 --- a/backend/qn-history-service/README.md +++ b/backend/qn-history-service/README.md @@ -24,6 +24,6 @@ ## After running -1. To view Question History Service documentation, go to http://localhost:3005/docs. +1. To view Question History Service documentation, go to http://localhost:3006/docs. -2. Using applications like Postman, you can interact with the Question History Service on port 3005. If you wish to change this, please update the `.env` file. +2. Using applications like Postman, you can interact with the Question History Service on port 3006. If you wish to change this, please update the `.env` file. diff --git a/backend/qn-history-service/server.ts b/backend/qn-history-service/server.ts index 0076a9289f..e42531d31d 100644 --- a/backend/qn-history-service/server.ts +++ b/backend/qn-history-service/server.ts @@ -1,7 +1,7 @@ import app from "./app.ts"; import connectDB from "./config/db.ts"; -const PORT = process.env.SERVICE_PORT || 3005; +const PORT = process.env.SERVICE_PORT || 3006; if (process.env.NODE_ENV !== "test") { connectDB() diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 10dc518c3a..a76c3725e9 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -103,7 +103,7 @@ services: build: ./backend/qn-history-service environment: - NODE_ENV=test - - SERVICE_PORT=3005 + - SERVICE_PORT=3006 - MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ depends_on: - test-mongo diff --git a/docker-compose.yml b/docker-compose.yml index cc1b072dd6..7c0538e3bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,7 +92,7 @@ services: - CHOKIDAR_USEPOLLING=true env_file: ./backend/qn-history-service/.env ports: - - 3005:3005 + - 3006:3006 depends_on: - qn-history-service-mongo - user-service diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index 18e283f415..144e61990e 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -2,7 +2,7 @@ import axios from "axios"; const usersUrl = "http://localhost:3001/api"; const questionsUrl = "http://localhost:3000/api/questions"; -const qnHistoriesUrl = "http://localhost:3005/api/qnhistories"; +const qnHistoriesUrl = "http://localhost:3006/api/qnhistories"; export const questionClient = axios.create({ baseURL: questionsUrl, From 258817a10e9f2860ab8621b390c93a3aeea15c18 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Fri, 1 Nov 2024 11:50:31 +0800 Subject: [PATCH 062/192] Change code template input field to monospace font --- frontend/src/components/QuestionCodeTemplates/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/QuestionCodeTemplates/index.tsx b/frontend/src/components/QuestionCodeTemplates/index.tsx index 354687172e..c1506b6118 100644 --- a/frontend/src/components/QuestionCodeTemplates/index.tsx +++ b/frontend/src/components/QuestionCodeTemplates/index.tsx @@ -30,7 +30,7 @@ const QuestionCodeTemplates: React.FC = ({ const [selectedLanguage, setSelectedLanguage] = useState("python"); const handleLanguageChange = ( - event: React.MouseEvent, + _: React.MouseEvent, language: string ) => { if (language) { @@ -115,6 +115,11 @@ const QuestionCodeTemplates: React.FC = ({ variant="outlined" multiline rows={8} + sx={{ + "& .MuiOutlinedInput-root": { + fontFamily: "monospace", + }, + }} value={codeTemplates[selectedLanguage]} onChange={handleCodeChange} onKeyDown={handleTabKeys} From 6d3f01c1f84d3eb5a5f2ecd3548605562db7be58 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Fri, 1 Nov 2024 19:19:01 +0800 Subject: [PATCH 063/192] Customise cursor style --- .../src/handlers/websocketHandler.ts | 42 ++-- frontend/src/components/CodeEditor/index.tsx | 34 +++- frontend/src/contexts/MatchContext.tsx | 10 +- frontend/src/pages/CollabSandbox/index.tsx | 28 ++- frontend/src/utils/collabCursor.ts | 184 +++++++----------- frontend/src/utils/collabSocket.ts | 47 ++++- 6 files changed, 200 insertions(+), 145 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index ba760d5cc7..c61e0dc02d 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -13,6 +13,7 @@ enum CollabEvents { PUSH_UPDATES = "push_updates", PULL_UPDATES = "pull_updates", + INIT_DOCUMENT = "init_document", GET_DOCUMENT = "get_document", // Send @@ -86,10 +87,28 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { // Adapted from https://codemirror.net/examples/collab/ and https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets let updates: Update[] = []; // updates.length = current version -let doc = Text.of(["Start document"]); +let doc = Text.of([""]); let pendingPullUpdatesRequests: ((updates: Update[]) => void)[] = []; const handleCodeEditorEvents = (socket: Socket) => { + socket.on( + CollabEvents.INIT_DOCUMENT, + (template: string, callback: () => void) => { + if (!doc.toString()) { + doc = Text.of([template]); + } + callback(); + } + ); + + socket.on(CollabEvents.GET_DOCUMENT, () => { + socket.emit( + CollabEvents.GET_DOCUMENT_RESPONSE, + updates.length, + doc.toString() + ); + }); + socket.on(CollabEvents.PULL_UPDATES, (version: number) => { if (version < updates.length) { // send the new updates @@ -111,13 +130,18 @@ const handleCodeEditorEvents = (socket: Socket) => { // received new updates, notify any pending pullUpdates requests socket.on( CollabEvents.PUSH_UPDATES, - (version: number, newUpdates: string, callback: () => void) => { + async ( + version: number, + newUpdates: string, + roomId: string, + callback: () => void + ) => { let docUpdates = JSON.parse(newUpdates) as readonly Update[]; try { // If the given version is the latest version, apply the new updates. // Else, rebase updates first. - if (version != updates.length) { + if (version < updates.length) { docUpdates = rebaseUpdates(docUpdates, updates.slice(version)); } @@ -129,6 +153,10 @@ const handleCodeEditorEvents = (socket: Socket) => { effects: update.effects, }); doc = changes.apply(doc); + + await redisClient.set(`collaboration:${roomId}`, doc.toString(), { + EX: EXPIRY_TIME, + }); } callback(); @@ -141,12 +169,4 @@ const handleCodeEditorEvents = (socket: Socket) => { } } ); - - socket.on(CollabEvents.GET_DOCUMENT, () => { - socket.emit( - CollabEvents.GET_DOCUMENT_RESPONSE, - updates.length, - doc.toString() - ); - }); }; diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 553b1bb30b..90c0bb47c5 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -6,6 +6,7 @@ import { EditorState } from "@codemirror/state"; import { useEffect, useState } from "react"; import { getDocument, + initDocument, peerExtension, removeListeners, } from "../../utils/collabSocket"; @@ -15,6 +16,9 @@ import { cursorExtension } from "../../utils/collabCursor"; interface CodeEditorProps { uid: string; username: string; + language: string; + template?: string; + roomId?: string; isReadOnly?: boolean; } @@ -23,8 +27,21 @@ type CodeEditorState = { doc: string | null; }; +const languageSupport = { + Python: langs.python(), + Java: langs.java(), + C: langs.c(), +}; + const CodeEditor: React.FC = (props) => { - const { uid, username, isReadOnly = false } = props; + const { + uid, + username, + language, + template = "", + roomId = "", + isReadOnly = false, + } = props; const [codeEditorState, setCodeEditorState] = useState({ version: null, @@ -32,8 +49,19 @@ const CodeEditor: React.FC = (props) => { }); useEffect(() => { + if (isReadOnly) { + setCodeEditorState({ + version: 0, + doc: template, + }); + return; + } + const fetchDocument = async () => { try { + if (template) { + await initDocument(template); + } const { version, doc } = await getDocument(); setCodeEditorState({ version: version, @@ -61,8 +89,8 @@ const CodeEditor: React.FC = (props) => { id="codeEditor" extensions={[ basicSetup(), - langs.c(), - peerExtension(codeEditorState.version, uid), + languageSupport[language as keyof typeof languageSupport], + peerExtension(codeEditorState.version, uid, roomId), cursorExtension(uid, username), EditorView.editable.of(!isReadOnly), EditorState.readOnly.of(isReadOnly), diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index c15a0f5e52..bac689821c 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -81,12 +81,12 @@ type MatchContextType = { matchingTimeout: () => void; matchOfferTimeout: () => void; verifyMatchStatus: () => void; - getMatchId: () => string | null; handleEndSessionClick: () => void; handleRejectEndSession: () => void; handleConfirmEndSession: () => void; matchUser: MatchUser | null; matchCriteria: MatchCriteria | null; + matchId: string | null; partner: MatchUser | null; matchPending: boolean; loading: boolean; @@ -489,13 +489,9 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { ); }; - const getMatchId = () => { - return matchId; - }; - const handleEndSessionClick = () => { setIsEndSessionModalOpen(true); - } + }; const handleRejectEndSession = () => { setIsEndSessionModalOpen(false); @@ -517,12 +513,12 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { matchingTimeout, matchOfferTimeout, verifyMatchStatus, - getMatchId, handleEndSessionClick, handleRejectEndSession, handleConfirmEndSession, matchUser, matchCriteria, + matchId, partner, matchPending, loading, diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index b858392371..096028ec1d 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -21,6 +21,7 @@ import reducer, { import QuestionDetailComponent from "../../components/QuestionDetail"; import { Navigate } from "react-router-dom"; import CodeEditor from "../../components/CodeEditor"; +import { join, leave } from "../../utils/collabSocket"; const CollabSandbox: React.FC = () => { const [showErrorScreen, setShowErrorScreen] = useState(false); @@ -32,11 +33,12 @@ const CollabSandbox: React.FC = () => { const { verifyMatchStatus, - getMatchId, handleRejectEndSession, handleConfirmEndSession, matchUser, partner, + matchCriteria, + matchId, loading, isEndSessionModalOpen, questionId, @@ -57,9 +59,11 @@ const CollabSandbox: React.FC = () => { getQuestionById(questionId, dispatch); // TODO - // use getMatchId() as the room id in the collab service - console.log(getMatchId()); + // use matchId as the room id in the collab service + console.log(matchId); + join(matchId); + return () => leave(matchId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -81,7 +85,7 @@ const CollabSandbox: React.FC = () => { return ; } - if (!matchUser || !partner) { + if (!matchUser || !partner || !matchCriteria || !matchId) { return ; } @@ -161,7 +165,21 @@ const CollabSandbox: React.FC = () => { sx={{ display: "flex", flexDirection: "column", height: "100%" }} > - + Test cases and chat tabs diff --git a/frontend/src/utils/collabCursor.ts b/frontend/src/utils/collabCursor.ts index c0391a9d4c..310f719ac6 100644 --- a/frontend/src/utils/collabCursor.ts +++ b/frontend/src/utils/collabCursor.ts @@ -15,34 +15,45 @@ export interface Cursor { to: number; } -class TooltipWidget extends WidgetType { - private name = "John"; - private suffix = ""; +class CursorWidget extends WidgetType { + private username: string; + private colorClass: string; - constructor(name: string, color: number) { + constructor(username: string, color: number) { super(); - this.suffix = `${(color % 8) + 1}`; - this.name = name; + this.colorClass = `cm-cursor-color-${color}`; + this.username = username; } toDOM() { - const dom = document.createElement("div"); - dom.className = "cm-tooltip-none"; - - const cursor_tooltip = document.createElement("div"); - cursor_tooltip.className = `cm-tooltip-cursor cm-tooltip cm-tooltip-above cm-tooltip-${this.suffix}`; - cursor_tooltip.textContent = this.name; - - const cursor_tooltip_arrow = document.createElement("div"); - cursor_tooltip_arrow.className = "cm-tooltip-arrow"; - - cursor_tooltip.appendChild(cursor_tooltip_arrow); - dom.appendChild(cursor_tooltip); - return dom; - } - - ignoreEvent() { - return false; + const cursorRoot = document.createElement("div"); + cursorRoot.className = "cm-cursor-root"; + + const cursor = document.createElement("div"); + cursor.className = `cm-cursor-display ${this.colorClass}`; + cursorRoot.appendChild(cursor); + + const cursorLabel = document.createElement("div"); + cursorLabel.className = `cm-cursor-label ${this.colorClass}`; + cursorLabel.textContent = this.username; + cursorRoot.appendChild(cursorLabel); + + let labelTimeout = setTimeout(() => { + cursorLabel.style.display = "none"; + }, 2000); + + cursor.addEventListener("mouseenter", () => { + clearTimeout(labelTimeout); + cursorLabel.style.display = "block"; + }); + + cursor.addEventListener("mouseleave", () => { + labelTimeout = setTimeout(() => { + cursorLabel.style.display = "none"; + }, 2000); + }); + + return cursorRoot; } } @@ -56,35 +67,36 @@ const cursorStateField = StateField.define({ let cursorTransactions = prevCursorState.map(transaction.changes); for (const effect of transaction.effects) { if (effect.is(updateCursor)) { - const addUpdates = []; + const cursorUpdates = []; + if (!cursors.has(effect.value.uid)) { - cursors.set(effect.value.uid, cursors.size); + cursors.set(effect.value.uid, cursors.size + 1); } + if (effect.value.from !== effect.value.to) { // highlight selected text - addUpdates.push( + cursorUpdates.push( Decoration.mark({ - class: `cm-highlight-${(cursors.get(effect.value.uid)! % 8) + 1}`, + class: `cm-highlight-color-${cursors.get(effect.value.uid)!}`, uid: effect.value.uid, }).range(effect.value.from, effect.value.to) ); } - addUpdates.push( + cursorUpdates.push( Decoration.widget({ - widget: new TooltipWidget( + widget: new CursorWidget( effect.value.username, cursors.get(effect.value.uid)! ), - block: false, uid: effect.value.uid, - }).range(effect.value.to, effect.value.to) + }).range(effect.value.to) ); // ensure only the latest cursor position and/or selection is displayed cursorTransactions = cursorTransactions.update({ - add: addUpdates, - filter: (_from, _to, value) => value?.spec?.uid !== effect.value.uid, + add: cursorUpdates, + filter: (_from, _to, value) => value.spec.uid !== effect.value.uid, }); } } @@ -94,95 +106,39 @@ const cursorStateField = StateField.define({ }); const cursorBaseTheme = EditorView.baseTheme({ - ".cm-tooltip.cm-tooltip-cursor": { - color: "white", - border: "none", - padding: "2px 7px", - borderRadius: "4px", - position: "absolute", - marginTop: "-40px", - marginLeft: "-14px", - "& .cm-tooltip-arrow:after": { - borderTopColor: "transparent", - }, - zIndex: "1000000", - }, - ".cm-tooltip-none": { + ".cm-cursor-root": { + display: "inline-block", width: "0px", height: "0px", - display: "inline-block", - }, - ".cm-highlight-1": { - backgroundColor: "#6666BB55", }, - ".cm-highlight-2": { - backgroundColor: "#F76E6E55", - }, - ".cm-highlight-3": { - backgroundColor: "#0CDA6255", - }, - ".cm-highlight-4": { - backgroundColor: "#0CC5DA55", - }, - ".cm-highlight-5": { - backgroundColor: "#0C51DA55", - }, - ".cm-highlight-6": { - backgroundColor: "#980CDA55", - }, - ".cm-highlight-7": { - backgroundColor: "#DA0CBB55", - }, - ".cm-highlight-8": { - backgroundColor: "#DA800C55", - }, - ".cm-tooltip-1": { - backgroundColor: "#66b !important", - "& .cm-tooltip-arrow:before": { - borderTopColor: "#66b !important", - }, - }, - ".cm-tooltip-2": { - backgroundColor: "#F76E6E !important", - "& .cm-tooltip-arrow:before": { - borderTopColor: "#F76E6E !important", - }, - }, - ".cm-tooltip-3": { - backgroundColor: "#0CDA62 !important", - "& .cm-tooltip-arrow:before": { - borderTopColor: "#0CDA62 !important", - }, + ".cm-cursor-display": { + border: "none", + width: "0.5px", + height: "18.5px", + position: "absolute", + marginTop: "-14.5px", + marginLeft: "0px", }, - ".cm-tooltip-4": { - backgroundColor: "#0CC5DA !important", - "& .cm-tooltip-arrow:before": { - borderTopColor: "#0CC5DA !important", - }, + ".cm-cursor-label": { + color: "white", + borderRadius: "4px 4px 4px 0px", + padding: "2px 4px", + fontSize: "12px", + position: "absolute", + marginTop: "-35px", + marginLeft: "0px", }, - ".cm-tooltip-5": { - backgroundColor: "#0C51DA !important", - "& .cm-tooltip-arrow:before": { - borderTopColor: "#0C51DA !important", - }, + ".cm-cursor-color-1": { + backgroundColor: "#f6a1a1", }, - ".cm-tooltip-6": { - backgroundColor: "#980CDA !important", - "& .cm-tooltip-arrow:before": { - borderTopColor: "#980CDA !important", - }, + ".cm-cursor-color-2": { + backgroundColor: "#d6a3e8", }, - ".cm-tooltip-7": { - backgroundColor: "#DA0CBB !important", - "& .cm-tooltip-arrow:before": { - borderTopColor: "#DA0CBB !important", - }, + ".cm-highlight-color-1": { + backgroundColor: "rgba(246, 161, 161, 0.3)", }, - ".cm-tooltip-8": { - backgroundColor: "#DA800C !important", - "& .cm-tooltip-arrow:before": { - borderTopColor: "#DA800C !important", - }, + ".cm-highlight-color-2": { + backgroundColor: "rgba(214, 163, 232, 0.3)", }, }); diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index ced6e6551a..21192010cb 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -14,8 +14,12 @@ import { updateCursor, Cursor } from "./collabCursor"; enum CollabEvents { // Send + JOIN = "join", + LEAVE = "leave", + PUSH_UPDATES = "push_updates", PULL_UPDATES = "pull_updates", + INIT_DOCUMENT = "init_document", GET_DOCUMENT = "get_document", // Receive @@ -23,11 +27,16 @@ enum CollabEvents { GET_DOCUMENT_RESPONSE = "get_document_response", } -const collabSocket = io("http://localhost:3003"); +const COLLAB_SOCKET_URL = "http://localhost:3003"; +const collabSocket = io(COLLAB_SOCKET_URL, { + reconnectionAttempts: 3, + autoConnect: false, +}); const pushUpdates = ( version: number, - fullUpdates: readonly Update[] + fullUpdates: readonly Update[], + roomId: string ): Promise => { const updates = fullUpdates.map((update) => ({ clientID: update.clientID, // client who made the update @@ -40,6 +49,7 @@ const pushUpdates = ( CollabEvents.PUSH_UPDATES, version, JSON.stringify(updates), + roomId, () => resolve() ); }); @@ -54,10 +64,16 @@ const pullUpdates = (version: number): Promise => { }); }).then((updates) => updates.map((update) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const effects: StateEffect[] = []; update.effects?.forEach((effect) => { - if (effect.value?.uid && effect.value?.from) { + if ( + effect.value.uid && + effect.value.username && + effect.value.from && + effect.value.to + ) { const cursor: Cursor = { uid: effect.value.uid, username: effect.value.username, @@ -77,6 +93,23 @@ const pullUpdates = (version: number): Promise => { ); }; +export const join = (matchId: string | null) => { + collabSocket.connect(); + collabSocket.emit(CollabEvents.JOIN, matchId); +}; + +export const leave = (matchId: string | null) => { + collabSocket.emit(CollabEvents.LEAVE, matchId); + collabSocket.disconnect(); +}; + +export const initDocument = (template: string): Promise => { + return new Promise((resolve) => { + console.log("emit init document"); + collabSocket.emit(CollabEvents.INIT_DOCUMENT, template, () => resolve()); + }); +}; + export const getDocument = (): Promise<{ version: number; doc: Text }> => { return new Promise((resolve) => { collabSocket.emit(CollabEvents.GET_DOCUMENT); @@ -94,7 +127,11 @@ export const getDocument = (): Promise<{ version: number; doc: Text }> => { }; // handles push and pull updates -export const peerExtension = (startVersion: number, uid: string) => { +export const peerExtension = ( + startVersion: number, + uid: string, + roomId: string +) => { const plugin = ViewPlugin.fromClass( class { private pushingUpdates = false; // to ensure only one running push request @@ -117,7 +154,7 @@ export const peerExtension = (startVersion: number, uid: string) => { } this.pushingUpdates = true; const version = getSyncedVersion(this.view.state); - await pushUpdates(version, updates); + await pushUpdates(version, updates, roomId); this.pushingUpdates = false; // check if there are still updates to push (failed / new updates) From 2cacd12d766c61bc9df511e01cef14279e99d3ff Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Fri, 1 Nov 2024 20:43:29 +0800 Subject: [PATCH 064/192] Remove decoration for user's own cursor --- frontend/src/components/CodeEditor/index.tsx | 5 +- frontend/src/pages/CollabSandbox/index.tsx | 9 +- frontend/src/utils/collabCursor.ts | 92 +++++++++----------- 3 files changed, 50 insertions(+), 56 deletions(-) diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 90c0bb47c5..82ec8b48a7 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -83,8 +83,9 @@ const CodeEditor: React.FC = (props) => { return ( { - + ({ + flex: 1, + width: "100%", + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + })} + > (); -const cursors = new Map(); +const cursorStateField = (uid: string): StateField => { + return StateField.define({ + create: () => Decoration.none, + update: (prevCursorState, transaction) => { + let cursorTransactions = prevCursorState.map(transaction.changes); + for (const effect of transaction.effects) { + // check for partner's cursor updates + if (effect.is(updateCursor) && effect.value.uid !== uid) { + const cursorUpdates = []; + + if (effect.value.from !== effect.value.to) { + // highlight selected text + cursorUpdates.push( + Decoration.mark({ + class: "cm-highlight-color", + uid: effect.value.uid, + }).range(effect.value.from, effect.value.to) + ); + } -const cursorStateField = StateField.define({ - create: () => Decoration.none, - update: (prevCursorState, transaction) => { - let cursorTransactions = prevCursorState.map(transaction.changes); - for (const effect of transaction.effects) { - if (effect.is(updateCursor)) { - const cursorUpdates = []; - - if (!cursors.has(effect.value.uid)) { - cursors.set(effect.value.uid, cursors.size + 1); - } - - if (effect.value.from !== effect.value.to) { - // highlight selected text cursorUpdates.push( - Decoration.mark({ - class: `cm-highlight-color-${cursors.get(effect.value.uid)!}`, + Decoration.widget({ + widget: new CursorWidget(effect.value.username), uid: effect.value.uid, - }).range(effect.value.from, effect.value.to) + }).range(effect.value.to) ); - } - cursorUpdates.push( - Decoration.widget({ - widget: new CursorWidget( - effect.value.username, - cursors.get(effect.value.uid)! - ), - uid: effect.value.uid, - }).range(effect.value.to) - ); - - // ensure only the latest cursor position and/or selection is displayed - cursorTransactions = cursorTransactions.update({ - add: cursorUpdates, - filter: (_from, _to, value) => value.spec.uid !== effect.value.uid, - }); + // ensure only the latest cursor position and/or selection is displayed + cursorTransactions = cursorTransactions.update({ + add: cursorUpdates, + filter: (_from, _to, value) => value.spec.uid !== effect.value.uid, + }); + } } - } - return cursorTransactions; - }, - provide: (field) => EditorView.decorations.from(field), -}); + return cursorTransactions; + }, + provide: (field) => EditorView.decorations.from(field), + }); +}; const cursorBaseTheme = EditorView.baseTheme({ ".cm-cursor-root": { @@ -128,23 +120,17 @@ const cursorBaseTheme = EditorView.baseTheme({ marginTop: "-35px", marginLeft: "0px", }, - ".cm-cursor-color-1": { + ".cm-cursor-color": { backgroundColor: "#f6a1a1", }, - ".cm-cursor-color-2": { - backgroundColor: "#d6a3e8", - }, - ".cm-highlight-color-1": { + ".cm-highlight-color": { backgroundColor: "rgba(246, 161, 161, 0.3)", }, - ".cm-highlight-color-2": { - backgroundColor: "rgba(214, 163, 232, 0.3)", - }, }); export const cursorExtension = (uid: string, username: string) => { return [ - cursorStateField, // handles cursor positions and highlights + cursorStateField(uid), // handles cursor positions and highlights cursorBaseTheme, // provides cursor styling // detects cursor updates EditorView.updateListener.of((update) => { From 46be3f75b65d237cce58bf8095ea836f46c897a8 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sat, 2 Nov 2024 01:23:05 +0800 Subject: [PATCH 065/192] Store collab sessions by room id --- backend/README.md | 12 +- .../src/handlers/websocketHandler.ts | 112 +++++++++++++----- backend/user-service/README.md | 8 -- frontend/src/components/CodeEditor/index.tsx | 14 ++- frontend/src/pages/CollabSandbox/index.tsx | 22 +++- frontend/src/utils/collabSocket.ts | 56 +++++---- 6 files changed, 148 insertions(+), 76 deletions(-) diff --git a/backend/README.md b/backend/README.md index bb3dc6ba1c..ea15cc12e4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,11 +2,19 @@ > Before proceeding to each microservice for more instructions: -1. Set up cloud MongoDB if not using docker. We recommend this if you are just testing out each microservice separately to avoid needing to manually set up multiple instances of local MongoDB. Else, if you are using docker-compose.yml to run PeerPrep, check out the READMEs in the different backend microservices to set up the env for the local MongoDB instances. +1. Set up cloud MongoDB if you are not using Docker. We recommend this if you are just testing out each microservice separately to avoid needing to manually set up multiple instances of local MongoDB. Otherwise, if you are using `docker-compose.yml` to run PeerPrep, check out the READMEs in the different backend microservices to set up the `.env` files for the local MongoDB instances. 2. Set up Firebase. -3. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. +3. For the microservices that use Redis, to view the contents stored: + + 1. Go to [http://localhost:5540](http://localhost:5540). + + 2. Click on "Add Redis Database". + + 3. Enter `host.docker.internal` as the Host. + +4. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. ## Setting-up cloud MongoDB (in production) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index c61e0dc02d..4f6a733ea6 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -7,7 +7,7 @@ import { rebaseUpdates, Update } from "@codemirror/collab"; enum CollabEvents { // Receive JOIN = "join", - CHANGE = "change", + // CHANGE = "change", LEAVE = "leave", DISCONNECT = "disconnect", @@ -18,9 +18,9 @@ enum CollabEvents { // Send ROOM_FULL = "room_full", - CONNECTED = "connected", + USER_CONNECTED = "user_connected", NEW_USER_CONNECTED = "new_user_connected", - CODE_CHANGE = "code_change", + // CODE_CHANGE = "code_change", PARTNER_LEFT = "partner_left", PARTNER_DISCONNECTED = "partner_disconnected", @@ -30,8 +30,16 @@ enum CollabEvents { const EXPIRY_TIME = 3600; +interface CollabSession { + updates: Update[]; // updates.length = current version + doc: Text; + pendingPullUpdatesRequests: ((updates: Update[]) => void)[]; +} + +const collabSessions = new Map(); + export const handleWebsocketCollabEvents = (socket: Socket) => { - socket.on(CollabEvents.JOIN, async ({ roomId }) => { + socket.on(CollabEvents.JOIN, async (roomId: string) => { if (!roomId) { return; } @@ -46,31 +54,43 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { socket.data.roomId = roomId; // in case of disconnect, send the code to the user when he rejoins - const code = await redisClient.get(`collaboration:${roomId}`); - socket.emit(CollabEvents.CONNECTED, { code: code ? code : "" }); + const collabSession = await redisClient.get(`collaboration:${roomId}`); + if (collabSession) { + if (!collabSessions.has(roomId)) { + collabSessions.set(roomId, JSON.parse(collabSession) as CollabSession); + } + } else { + initCollabSession(roomId); + } + socket.emit(CollabEvents.USER_CONNECTED); // inform the other user that a new user has joined socket.to(roomId).emit(CollabEvents.NEW_USER_CONNECTED); }); - socket.on(CollabEvents.CHANGE, async ({ roomId, code }) => { - if (!roomId || !code) { - return; - } + // socket.on(CollabEvents.CHANGE, async (roomId: string, code: string) => { + // if (!roomId || !code) { + // return; + // } - await redisClient.set(`collaboration:${roomId}`, code, { - EX: EXPIRY_TIME, - }); - socket.to(roomId).emit(CollabEvents.CODE_CHANGE, { code }); - }); + // await redisClient.set(`collaboration:${roomId}`, code, { + // EX: EXPIRY_TIME, + // }); + // socket.to(roomId).emit(CollabEvents.CODE_CHANGE, code); + // }); - socket.on(CollabEvents.LEAVE, ({ roomId }) => { + socket.on(CollabEvents.LEAVE, (roomId: string) => { if (!roomId) { return; } socket.leave(roomId); - socket.to(roomId).emit(CollabEvents.PARTNER_LEFT); + const room = io.sockets.adapter.rooms.get(roomId); + if (room?.size === 0) { + collabSessions.delete(roomId); + } else { + socket.to(roomId).emit(CollabEvents.PARTNER_LEFT); + } }); socket.on(CollabEvents.DISCONNECT, () => { @@ -86,22 +106,17 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { /* Code Editor Events */ // Adapted from https://codemirror.net/examples/collab/ and https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets -let updates: Update[] = []; // updates.length = current version -let doc = Text.of([""]); -let pendingPullUpdatesRequests: ((updates: Update[]) => void)[] = []; - const handleCodeEditorEvents = (socket: Socket) => { socket.on( CollabEvents.INIT_DOCUMENT, - (template: string, callback: () => void) => { - if (!doc.toString()) { - doc = Text.of([template]); - } + (roomId: string, template: string, callback: () => void) => { + initCollabSession(roomId, template); callback(); } ); - socket.on(CollabEvents.GET_DOCUMENT, () => { + socket.on(CollabEvents.GET_DOCUMENT, (roomId: string) => { + const { updates, doc } = initCollabSession(roomId); socket.emit( CollabEvents.GET_DOCUMENT_RESPONSE, updates.length, @@ -109,7 +124,8 @@ const handleCodeEditorEvents = (socket: Socket) => { ); }); - socket.on(CollabEvents.PULL_UPDATES, (version: number) => { + socket.on(CollabEvents.PULL_UPDATES, (roomId: string, version: number) => { + const { updates, pendingPullUpdatesRequests } = initCollabSession(roomId); if (version < updates.length) { // send the new updates socket.emit( @@ -131,11 +147,13 @@ const handleCodeEditorEvents = (socket: Socket) => { socket.on( CollabEvents.PUSH_UPDATES, async ( + roomId: string, version: number, newUpdates: string, - roomId: string, callback: () => void ) => { + const { updates, doc, pendingPullUpdatesRequests } = + initCollabSession(roomId); let docUpdates = JSON.parse(newUpdates) as readonly Update[]; try { @@ -152,11 +170,21 @@ const handleCodeEditorEvents = (socket: Socket) => { changes: changes, effects: update.effects, }); - doc = changes.apply(doc); - await redisClient.set(`collaboration:${roomId}`, doc.toString(), { - EX: EXPIRY_TIME, - }); + const updatedCollabSession = { + updates: updates, + doc: changes.apply(doc), + pendingPullUpdatesRequests: pendingPullUpdatesRequests, + }; + collabSessions.set(roomId, updatedCollabSession); + + await redisClient.set( + `collaboration:${roomId}`, + JSON.stringify(updatedCollabSession), + { + EX: EXPIRY_TIME, + } + ); } callback(); @@ -170,3 +198,23 @@ const handleCodeEditorEvents = (socket: Socket) => { } ); }; + +const initCollabSession = ( + roomId: string, + template?: string +): CollabSession => { + const collabSession = collabSessions.get(roomId); + if (!collabSession) { + collabSessions.set(roomId, { + updates: [], + doc: Text.of([template ? template : ""]), + pendingPullUpdatesRequests: [], + }); + } else if (template) { + collabSessions.set(roomId, { + ...collabSession, + doc: Text.of([template]), + }); + } + return collabSessions.get(roomId)!; +}; diff --git a/backend/user-service/README.md b/backend/user-service/README.md index 5bd2f732f3..0c04de5047 100644 --- a/backend/user-service/README.md +++ b/backend/user-service/README.md @@ -36,14 +36,6 @@ 5. A default admin account (`email: admin@gmail.com` and `password: Admin@123`) wil be created. If you wish to change the default credentials, update them in `.env`. Alternatively, you can also edit your credentials and user profile after you have created the default account. -6. To view the contents stored in Redis, - - 1. Go to [http://localhost:5540](http://localhost:5540). - - 2. Click on "Add Redis Database". - - 3. Enter `host.internal.docker` as the Host. - ## Running User Service Individually > Make sure you have the cloud MongoDB URI in your .env file and set NODE_ENV to production already. diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 82ec8b48a7..b628f5cbe6 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -8,7 +8,6 @@ import { getDocument, initDocument, peerExtension, - removeListeners, } from "../../utils/collabSocket"; import Loader from "../Loader"; import { cursorExtension } from "../../utils/collabCursor"; @@ -58,11 +57,16 @@ const CodeEditor: React.FC = (props) => { } const fetchDocument = async () => { + if (!roomId) { + return; + } + try { if (template) { - await initDocument(template); + await initDocument(roomId, template); } - const { version, doc } = await getDocument(); + + const { version, doc } = await getDocument(roomId); setCodeEditorState({ version: version, doc: doc.toString(), @@ -73,8 +77,6 @@ const CodeEditor: React.FC = (props) => { }; fetchDocument(); - - return () => removeListeners(); }, []); if (codeEditorState.version === null || codeEditorState.doc === null) { @@ -91,7 +93,7 @@ const CodeEditor: React.FC = (props) => { extensions={[ basicSetup(), languageSupport[language as keyof typeof languageSupport], - peerExtension(codeEditorState.version, uid, roomId), + peerExtension(roomId, codeEditorState.version, uid), cursorExtension(uid, username), EditorView.editable.of(!isReadOnly), EditorState.readOnly.of(isReadOnly), diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 520d89c5b3..268f677f0b 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -24,6 +24,7 @@ import CodeEditor from "../../components/CodeEditor"; import { join, leave } from "../../utils/collabSocket"; const CollabSandbox: React.FC = () => { + const [connected, setConnected] = useState(false); const [showErrorScreen, setShowErrorScreen] = useState(false); const match = useMatch(); @@ -58,12 +59,23 @@ const CollabSandbox: React.FC = () => { } getQuestionById(questionId, dispatch); - // TODO - // use matchId as the room id in the collab service - console.log(matchId); - join(matchId); + if (!matchId || connected) { + return; + } + + const connectToCollabSession = async () => { + try { + await join(matchId); + setConnected(true); + } catch (error) { + console.error("Error connecting to collab session: ", error); + } + }; + + connectToCollabSession(); return () => leave(matchId); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -98,7 +110,7 @@ const CollabSandbox: React.FC = () => { ); } - if (!selectedQuestion) { + if (!selectedQuestion || !connected) { return ; } diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 21192010cb..c4958f7e2a 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -23,6 +23,8 @@ enum CollabEvents { GET_DOCUMENT = "get_document", // Receive + USER_CONNECTED = "user_connected", + PULL_UPDATES_RESPONSE = "pull_updates_response", GET_DOCUMENT_RESPONSE = "get_document_response", } @@ -34,9 +36,9 @@ const collabSocket = io(COLLAB_SOCKET_URL, { }); const pushUpdates = ( + roomId: string, version: number, - fullUpdates: readonly Update[], - roomId: string + fullUpdates: readonly Update[] ): Promise => { const updates = fullUpdates.map((update) => ({ clientID: update.clientID, // client who made the update @@ -47,17 +49,20 @@ const pushUpdates = ( return new Promise((resolve) => { collabSocket.emit( CollabEvents.PUSH_UPDATES, + roomId, version, JSON.stringify(updates), - roomId, () => resolve() ); }); }; -const pullUpdates = (version: number): Promise => { +const pullUpdates = ( + roomId: string, + version: number +): Promise => { return new Promise((resolve) => { - collabSocket.emit(CollabEvents.PULL_UPDATES, version); + collabSocket.emit(CollabEvents.PULL_UPDATES, roomId, version); collabSocket.once(CollabEvents.PULL_UPDATES_RESPONSE, (updates: string) => { resolve(JSON.parse(updates)); @@ -93,26 +98,36 @@ const pullUpdates = (version: number): Promise => { ); }; -export const join = (matchId: string | null) => { +export const join = (roomId: string): Promise => { collabSocket.connect(); - collabSocket.emit(CollabEvents.JOIN, matchId); + collabSocket.emit(CollabEvents.JOIN, roomId); + + return new Promise((resolve) => { + collabSocket.once(CollabEvents.USER_CONNECTED, () => resolve()); + }); }; -export const leave = (matchId: string | null) => { - collabSocket.emit(CollabEvents.LEAVE, matchId); +export const leave = (roomId: string) => { + collabSocket.emit(CollabEvents.LEAVE, roomId); collabSocket.disconnect(); }; -export const initDocument = (template: string): Promise => { +export const initDocument = ( + roomId: string, + template: string +): Promise => { return new Promise((resolve) => { - console.log("emit init document"); - collabSocket.emit(CollabEvents.INIT_DOCUMENT, template, () => resolve()); + collabSocket.emit(CollabEvents.INIT_DOCUMENT, roomId, template, () => + resolve() + ); }); }; -export const getDocument = (): Promise<{ version: number; doc: Text }> => { +export const getDocument = ( + roomId: string +): Promise<{ version: number; doc: Text }> => { return new Promise((resolve) => { - collabSocket.emit(CollabEvents.GET_DOCUMENT); + collabSocket.emit(CollabEvents.GET_DOCUMENT, roomId); collabSocket.once( CollabEvents.GET_DOCUMENT_RESPONSE, @@ -128,9 +143,9 @@ export const getDocument = (): Promise<{ version: number; doc: Text }> => { // handles push and pull updates export const peerExtension = ( + roomId: string, startVersion: number, - uid: string, - roomId: string + uid: string ) => { const plugin = ViewPlugin.fromClass( class { @@ -154,7 +169,7 @@ export const peerExtension = ( } this.pushingUpdates = true; const version = getSyncedVersion(this.view.state); - await pushUpdates(version, updates, roomId); + await pushUpdates(roomId, version, updates); this.pushingUpdates = false; // check if there are still updates to push (failed / new updates) @@ -166,7 +181,7 @@ export const peerExtension = ( async pull() { while (this.pullUpdates) { const version = getSyncedVersion(this.view.state); - const updates = await pullUpdates(version); // returns only if there are updates + const updates = await pullUpdates(roomId, version); // returns only if there are updates this.view.dispatch(receiveUpdates(this.view.state, updates)); } } @@ -187,8 +202,3 @@ export const peerExtension = ( plugin, ]; }; - -export const removeListeners = () => { - collabSocket.off(CollabEvents.PULL_UPDATES_RESPONSE); - collabSocket.off(CollabEvents.GET_DOCUMENT_RESPONSE); -}; From 6e8d8d56ce160d013a04b443d5c47a6869d7fdb3 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sat, 2 Nov 2024 15:33:12 +0800 Subject: [PATCH 066/192] resolve comment issues and add loader to prevent error msg rendering before actual data --- backend/matching-service/.env.sample | 2 ++ frontend/src/App.tsx | 9 +-------- frontend/src/pages/Profile/index.tsx | 9 +++++++++ frontend/src/pages/QuestionHistoryDetail/index.tsx | 11 +++++++++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/backend/matching-service/.env.sample b/backend/matching-service/.env.sample index 1cb58137db..30292db838 100644 --- a/backend/matching-service/.env.sample +++ b/backend/matching-service/.env.sample @@ -14,3 +14,5 @@ RABBITMQ_DEFAULT_PASS=password #comment out if use case is (1) RABBITMQ_ADDR=amqp://admin:password@rabbitmq:5672 #comment out if use case is (1) QUESTION_SERVICE_URL=http://question-service:3000/api/questions + +QN_HISTORY_SERVICE_URL=http://qn-history-service:3006/api/qnhistories diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index db2d37d413..99cea9a241 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -52,14 +52,7 @@ function App() { } /> - - - - } - /> + } /> }> }> } /> diff --git a/frontend/src/pages/Profile/index.tsx b/frontend/src/pages/Profile/index.tsx index f41efc44a3..c942af986d 100644 --- a/frontend/src/pages/Profile/index.tsx +++ b/frontend/src/pages/Profile/index.tsx @@ -16,12 +16,14 @@ import { import qnHistoryReducer, { getQnHistoryList, initialQHState } from "../../reducers/qnHistoryReducer"; import { grey } from "@mui/material/colors"; import { convertDateString } from "../../utils/sessionTime"; +import Loader from "../../components/Loader"; const rowsPerPage = 10; const ProfilePage: React.FC = () => { const [page, setPage] = useState(0); const [state, dispatch] = useReducer(qnHistoryReducer, initialQHState); + const [loading, setLoading] = useState(true); const navigate = useNavigate(); const { userId } = useParams<{ userId: string }>(); @@ -47,11 +49,14 @@ const ProfilePage: React.FC = () => { } = profile; useEffect(() => { + setLoading(true); if (!userId) { + setTimeout(() => setLoading(false), 500); return; } fetchUser(userId); + setTimeout(() => setLoading(false), 500); // eslint-disable-next-line react-hooks/exhaustive-deps }, [userId]); @@ -69,6 +74,10 @@ const ProfilePage: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => updateQnHistoryList(), [page]); + if (loading) { + return ; + } + if (!user) { return ( { const { qnHistoryId } = useParams<{ qnHistoryId: string }>(); const [qnhistState, qnhistDispatch] = useReducer(qnHistoryReducer, initialQHState); const [qnState, qnDispatch] = useReducer(reducer, initialState); + const [loading, setLoading] = useState(true); const navigate = useNavigate(); const auth = useAuth(); @@ -44,6 +46,7 @@ const QuestionHistoryDetail: React.FC = () => { if (qnhistState.selectedQnHistory) { getQuestionById(qnhistState.selectedQnHistory.questionId, qnDispatch); } + setTimeout(() => setLoading(false), 500); }, [qnhistState]) @@ -55,6 +58,10 @@ const QuestionHistoryDetail: React.FC = () => { } } + if (loading) { + return ; + } + if (!qnhistState.selectedQnHistory) { if (qnhistState.selectedQnHistoryError) { return ( @@ -72,7 +79,7 @@ const QuestionHistoryDetail: React.FC = () => { return ( - navigate(-1)}> + navigate(`/profile/${user?.id}`)}> Latest submission details From 5f1ade4a3c1dbe94acfb20398cb4a30903cf06e0 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sat, 2 Nov 2024 17:02:52 +0800 Subject: [PATCH 067/192] Add docs --- README.md | 7 +++ backend/communication-service/README.md | 54 ++++++++++++++++++ backend/communication-service/docs/image1.png | Bin 0 -> 63175 bytes backend/communication-service/docs/image2.png | Bin 0 -> 61725 bytes backend/communication-service/docs/image3.png | Bin 0 -> 62029 bytes backend/communication-service/docs/image4.png | Bin 0 -> 51711 bytes .../src/handlers/websocketHandler.ts | 2 +- 7 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 backend/communication-service/README.md create mode 100644 backend/communication-service/docs/image1.png create mode 100644 backend/communication-service/docs/image2.png create mode 100644 backend/communication-service/docs/image3.png create mode 100644 backend/communication-service/docs/image4.png diff --git a/README.md b/README.md index 7f05cfc064..0fdca5487a 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,15 @@ docker-compose down ## Useful links - User Service: http://localhost:3001 + - Question Service: http://localhost:3000 + - Matching Service: http://localhost:3002 + - Collab Service: http://localhost:3003 + - Code Execution Service: http://localhost:3004 + +- Communication Service: http://localhost:3005 + - Frontend: http://localhost:5173 diff --git a/backend/communication-service/README.md b/backend/communication-service/README.md new file mode 100644 index 0000000000..b0f30a74e9 --- /dev/null +++ b/backend/communication-service/README.md @@ -0,0 +1,54 @@ +# Communication Service Guide + +> Please ensure that you have completed the backend set-up [here](../README.md) before proceeding. + +## Setting-up Communication Service + +1. In the `communication-service` directory, create a copy of the `.env.sample` file and name it `.env`. + +## Running Communication Service Individually + +1. Open Command Line/Terminal and navigate into the `communication-service` directory. + +2. Run the command: `npm install`. This will install all the necessary dependencies. + +3. Run the command `npm start` to start the Communication Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. + +## Running Communication Service Individually with Docker + +1. Open the command line/terminal. + +2. Run the command `docker compose run -p 3005:3005 communication-service` to start up the communication service and its dependencies. + +## After Running + +1. Using applications like Postman, you can interact with the Communication Service on port 3005. If you wish to change this, please update the `.env` file. + +2. Setting up Socket.IO connection on Postman: + + - You should open 2 tabs on Postman to simulate 2 users in the Communication Service. + + - Select the `Socket.IO` option and set URL to `http://localhost:3005`. Click `Connect`. + + ![image1.png](./docs/image1.png) + + - Add the following events in the `Events` tab and listen to them. + + ![image2.png](./docs/image2.png) + + - To send a message, go to the `Message` tab and ensure that your message is being parsed as `JSON`. + + ![image3.png](./docs/image3.png) + + - In the `Event name` input, input the correct event name. Click on `Send` to send a message. + + ![image4.png](./docs/image4.png) + +## Events Available + +| Event Name | Description | Parameters | Response Event | +| --------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| **join** | Joins a communication rooms | `roomId` (string): ID of the room.
`username` (string): Username of the user that joined. | **user_joined**: Notify the other user that a new user has joined the room. | +| **send_text_message** | Sends a message to the other user | `roomId` (string): ID of the room.
`message` (string): Message to send.
`username` (string): User that sent the message.
`createdTime` (number): Time that the user sent the message. | **text_message_received**: Notify the user that a message is sent | +| **leave** | Leaves the communication room. | `roomId` (string): ID of the room to leave.
`username` (string): User that wants to leave. | **user_left**: To notify the user when one user leaves. | +| **disconnect** | Disconnects from the server. | None | **disconnected**: To notify the user when one user gets disconnected from the server. | diff --git a/backend/communication-service/docs/image1.png b/backend/communication-service/docs/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..e5b4447758b3c81c29babafefc3093711fc1fc93 GIT binary patch literal 63175 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

NDI(F)G$5LbEu)Kp%?*U4yEQ! zmy%~}!u?cPu5<*|Ct)8bWOoT~m}%frzad3n_}G*cn^ONgYFv%|NAi8R4B>l~ZzZ;= zGT)3pp*YQQth<{L?esrK^qwvR$217dqflAtRbA7 z+j>18mU@u@~`;Z+(3_19DEl-j6b!vST;RVTS%Wmi|;q*rp*OWlCdj%(+Y$A zzW^INBcf#`;?_7BA6~7$uj7|pg};Y(HGS=AUwFMQ!#_~P(7FMVaV5(wtOM2mq`W=W z@=Fv)R~D{4#PYayrt~S!vVx{$3Bh5iRg%RGL$x$vgCZD?q*=O$S5c3x zQa@d7KU^5$Wf&#?@7lE=_0#Km&EyP@m73(YSTz2f<`c4kL*56PPnpkgLG?nr5<=`O zbNDB*FGMZ%WNnUMGkibK9`#j%Mb+}5nRmUVc1-e2k)35W|JjxYy0XhAt3|xHx#A7i z3IhI~3hw=?*KRf{I6tb!nH21Nc`nLX`EaBAJ&$Mwk!)IOIzAIZUCnr6zna*Slym95 zyZ(Z&R2qR>`Pjjm`&A?f8?>2J;5xLc<|HkmvR9%qTP&KkUXVkH zTq(Q-?q)@j$eA`1OJ{zL_ zR8@u8@NnpYQB`sLSK2;K^&pIxVt#KUf1X{sb5Y2mYp_wxliDexRFA_=wLTjRwi8?f zAGcvTB?_kf^K(o-v&50-Rw})~=Ji8M6VEJ)JTW>p6G1Q** z#in|E8SdL?uZoe!s+XmZ*4w!CrCs=8k6Vd-{@71mHJf5;Jwe>j3-6`yF+OJNOOx=! zW=AwH`f9e?W({phiS(v;aX-vn*h+LpNrwD=PDA%}WY26rrrPL?SJQkg@uO33@2lff z_#jTv7nhF?6=_$U66-zQ8$R8C>os>uOlEQ02GUJ>L2w%pUIsNC*;NoD+7}=GD|Aaj zsZ82LwiLRwaLZbrfB4A$8v%2VJq(b=;I3zzzklEuGfI9}wz!o2To-uluj z{4lBFp0UzRhlNFp=0|37v71q`)%%g95MzVz-`Z7lr@lYth}q!Xxls7Coh=kJwQq8Tq!6|1Jj0;p)Nz8o0|F?MVGtzA7o6Kwo7_Q?t? l@&95mtZ5$YVqH>x-1F43v7>b!B;2V^pVB;;e!|@Ue*h9HGIIa` literal 0 HcmV?d00001 diff --git a/backend/communication-service/docs/image2.png b/backend/communication-service/docs/image2.png new file mode 100644 index 0000000000000000000000000000000000000000..e422ba04625e742fbf424f552944a9e9d9516e38 GIT binary patch literal 61725 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

JW?dF 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 Mi{DMTt>N;203|j~Y5)KL literal 0 HcmV?d00001 diff --git a/backend/communication-service/docs/image3.png b/backend/communication-service/docs/image3.png new file mode 100644 index 0000000000000000000000000000000000000000..6ad7ecd408a811bcc352b97f304a1c3d45d59d78 GIT binary patch literal 62029 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�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 literal 0 HcmV?d00001 diff --git a/backend/communication-service/docs/image4.png b/backend/communication-service/docs/image4.png new file mode 100644 index 0000000000000000000000000000000000000000..30ce8080692c73aae51409a9e050a9a48cfe577a GIT binary patch literal 51711 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 literal 0 HcmV?d00001 diff --git a/backend/communication-service/src/handlers/websocketHandler.ts b/backend/communication-service/src/handlers/websocketHandler.ts index 4dd329fd6e..79351ad925 100644 --- a/backend/communication-service/src/handlers/websocketHandler.ts +++ b/backend/communication-service/src/handlers/websocketHandler.ts @@ -42,7 +42,7 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { socket.leave(roomId); const createdTime = Date.now(); - socket.to(roomId).emit(CommunicationEvents.LEAVE, { + socket.to(roomId).emit(CommunicationEvents.USER_LEFT, { from: BOT_NAME, type: MessageTypes.BOT_GENERATED, message: `${username} has left the chat`, From 98658245f1a74c749210bdb0272479d9af07d246 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sat, 2 Nov 2024 21:19:11 +0800 Subject: [PATCH 068/192] resolve comment issues and setup tests --- backend/qn-history-service/jest.config.ts | 2 +- backend/qn-history-service/package-lock.json | 489 ++++++++++++++++++ backend/qn-history-service/package.json | 9 +- backend/qn-history-service/{ => src}/app.ts | 2 +- .../qn-history-service/{ => src}/config/db.ts | 0 .../controllers/questionHistoryController.ts | 5 + .../src/models/QnHistory.ts | 2 +- .../qn-history-service/{ => src}/server.ts | 0 .../tests/qnHistoryRoutes.spec.ts | 260 ++++++++++ backend/qn-history-service/tests/setup.ts | 25 + 10 files changed, 789 insertions(+), 5 deletions(-) rename backend/qn-history-service/{ => src}/app.ts (93%) rename backend/qn-history-service/{ => src}/config/db.ts (100%) rename backend/qn-history-service/{ => src}/server.ts (100%) create mode 100644 backend/qn-history-service/tests/qnHistoryRoutes.spec.ts create mode 100644 backend/qn-history-service/tests/setup.ts diff --git a/backend/qn-history-service/jest.config.ts b/backend/qn-history-service/jest.config.ts index 151d29ec19..4e7a55569f 100644 --- a/backend/qn-history-service/jest.config.ts +++ b/backend/qn-history-service/jest.config.ts @@ -137,7 +137,7 @@ const config: Config = { // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + setupFilesAfterEnv: ["./tests/setup.ts"], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, diff --git a/backend/qn-history-service/package-lock.json b/backend/qn-history-service/package-lock.json index cd26990e48..b0484bca52 100644 --- a/backend/qn-history-service/package-lock.json +++ b/backend/qn-history-service/package-lock.json @@ -20,15 +20,20 @@ }, "devDependencies": { "@eslint/js": "^9.13.0", + "@faker-js/faker": "^9.1.0", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.8.1", + "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "cross-env": "^7.0.3", "eslint": "^9.13.0", "globals": "^15.11.0", "jest": "^29.7.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", "tsx": "^4.19.2", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0" @@ -544,6 +549,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", @@ -1089,6 +1118,23 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@faker-js/faker": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.1.0.tgz", + "integrity": "sha512-GJvX9iM9PBtKScJVlXQ0tWpihK3i0pha/XAhzQa1hPK/ILLa1Wq3I63Ij7lRtqTwmdTxRCyrUhLC5Sly9SLbug==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1677,6 +1723,34 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1743,6 +1817,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", @@ -1848,6 +1929,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1909,6 +1997,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/swagger-ui-express": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", @@ -2230,6 +2342,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2303,6 +2428,13 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2316,6 +2448,20 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2552,6 +2698,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -2762,6 +2921,16 @@ "node": ">= 0.8" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2812,6 +2981,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2847,6 +3023,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -2985,6 +3168,27 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -3013,6 +3217,22 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.47", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.47.tgz", @@ -3485,6 +3705,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -3518,6 +3745,39 @@ "node": ">=16.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3636,6 +3896,21 @@ "node": ">= 6" } }, + "node_modules/formidable": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", + "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^2.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3892,6 +4167,16 @@ "node": ">= 0.4" } }, + "node_modules/hexoid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", + "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4201,6 +4486,25 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -4947,6 +5251,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4993,6 +5304,13 @@ "node": ">=10" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -6200,6 +6518,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz", + "integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^9.0.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6323,6 +6689,112 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsx": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", @@ -6496,6 +6968,13 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -6675,6 +7154,16 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/backend/qn-history-service/package.json b/backend/qn-history-service/package.json index a5b6a7d4bd..6bbc94c2b9 100644 --- a/backend/qn-history-service/package.json +++ b/backend/qn-history-service/package.json @@ -4,8 +4,8 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx server.ts", - "dev": "tsx watch server.ts", + "start": "tsx src/server.ts", + "dev": "tsx watch src/server.ts", "test": "cross-env NODE_ENV=test && jest", "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." @@ -15,15 +15,20 @@ "description": "", "devDependencies": { "@eslint/js": "^9.13.0", + "@faker-js/faker": "^9.1.0", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.8.1", + "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "cross-env": "^7.0.3", "eslint": "^9.13.0", "globals": "^15.11.0", "jest": "^29.7.0", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", "tsx": "^4.19.2", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0" diff --git a/backend/qn-history-service/app.ts b/backend/qn-history-service/src/app.ts similarity index 93% rename from backend/qn-history-service/app.ts rename to backend/qn-history-service/src/app.ts index 4b02f33a84..2dabadcf4f 100644 --- a/backend/qn-history-service/app.ts +++ b/backend/qn-history-service/src/app.ts @@ -5,7 +5,7 @@ import yaml from "yaml"; import swaggerUi from "swagger-ui-express"; import cors from "cors"; -import qnHistoryRoutes from "./src/routes/questionHistoryRoutes.ts"; +import qnHistoryRoutes from "./routes/questionHistoryRoutes.ts"; dotenv.config(); diff --git a/backend/qn-history-service/config/db.ts b/backend/qn-history-service/src/config/db.ts similarity index 100% rename from backend/qn-history-service/config/db.ts rename to backend/qn-history-service/src/config/db.ts diff --git a/backend/qn-history-service/src/controllers/questionHistoryController.ts b/backend/qn-history-service/src/controllers/questionHistoryController.ts index ee68c85317..701c64221a 100644 --- a/backend/qn-history-service/src/controllers/questionHistoryController.ts +++ b/backend/qn-history-service/src/controllers/questionHistoryController.ts @@ -131,6 +131,11 @@ export const readQnHistoryList = async ( return; } + if (!userId.match(MONGO_OBJ_ID_FORMAT)) { + res.status(400).json({ message: MONGO_OBJ_ID_MALFORMED_MESSAGE }); + return; + } + const filteredQnHistCount = await QnHistory.countDocuments({ userIds: userId, }); diff --git a/backend/qn-history-service/src/models/QnHistory.ts b/backend/qn-history-service/src/models/QnHistory.ts index f5805ae0ae..d62a9f85a5 100644 --- a/backend/qn-history-service/src/models/QnHistory.ts +++ b/backend/qn-history-service/src/models/QnHistory.ts @@ -6,7 +6,7 @@ export interface IQnHistory extends Document { title: string; submissionStatus: string; dateAttempted: Date; - timeTaken: Number; + timeTaken: number; code: string; language: string; createdAt: Date; diff --git a/backend/qn-history-service/server.ts b/backend/qn-history-service/src/server.ts similarity index 100% rename from backend/qn-history-service/server.ts rename to backend/qn-history-service/src/server.ts diff --git a/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts b/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts new file mode 100644 index 0000000000..998f17f325 --- /dev/null +++ b/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts @@ -0,0 +1,260 @@ +import { faker } from "@faker-js/faker"; +import supertest from "supertest"; +import app from "../src/app"; +import { + MONGO_OBJ_ID_MALFORMED_MESSAGE, + PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE, + PAGE_LIMIT_USERID_REQUIRED_MESSAGE, + QN_HIST_NOT_FOUND_MESSAGE, +} from "../src/utils/constants"; +import QnHistory from "../src/models/QnHistory"; + +const request = supertest(app); + +const BASE_URL = "/api/qnhistories"; + +faker.seed(0); + +describe("Qn History Routes", () => { + describe("POST / ", () => { + it("Creates new qn history", async () => { + const userIds = ["66f77e9f27ab3f794bdae664", "66f77e9f27ab3f794bdae665"]; + const questionId = "66f77e9f27ab3f794bdae666"; + const title = faker.lorem.lines(1); + const submissionStatus = "Attempted"; + const dateAttempted = new Date(); + const timeTaken = 0; + const language = "Python"; + const newQnHistory = { + userIds, + questionId, + title, + submissionStatus, + dateAttempted, + timeTaken, + language, + }; + + const res = await request.post(`${BASE_URL}`).send(newQnHistory); + + expect(res.status).toBe(201); + expect(res.body.qnHistory.userIds).toEqual(userIds); + expect(res.body.qnHistory.questionId).toBe(questionId); + expect(res.body.qnHistory.title).toBe(title); + expect(res.body.qnHistory.submissionStatus).toBe(submissionStatus); + expect(res.body.qnHistory.dateAttempted).toBe( + dateAttempted.toISOString() + ); + expect(res.body.qnHistory.timeTaken).toBe(timeTaken); + expect(res.body.qnHistory.language).toBe(language); + }); + }); + + describe("GET /", () => { + it("Reads existing question histories", async () => { + const qnHistLimit = 10; + const res = await request.get( + `${BASE_URL}?page=1&qnHistLimit=${qnHistLimit}&userId=66f77e9f27ab3f794bdae664` + ); + expect(res.status).toBe(200); + expect(res.body.qnHistories.length).toBeLessThanOrEqual(qnHistLimit); + }); + + it("Does not read without page", async () => { + const res = await request.get( + `${BASE_URL}?qnHistLimit=10&userId=66f77e9f27ab3f794bdae664` + ); + expect(res.status).toBe(400); + expect(res.body.message).toBe(PAGE_LIMIT_USERID_REQUIRED_MESSAGE); + }); + + it("Does not read without qnHistLimit", async () => { + const res = await request.get( + `${BASE_URL}?page=1&userId=66f77e9f27ab3f794bdae664` + ); + expect(res.status).toBe(400); + expect(res.body.message).toBe(PAGE_LIMIT_USERID_REQUIRED_MESSAGE); + }); + + it("Does not read without userId", async () => { + const res = await request.get(`${BASE_URL}?page=1&qnHistLimit=10`); + expect(res.status).toBe(400); + expect(res.body.message).toBe(PAGE_LIMIT_USERID_REQUIRED_MESSAGE); + }); + + it("Does not read with negative page", async () => { + const res = await request.get( + `${BASE_URL}?page=-1&qnHistLimit=10&userId=66f77e9f27ab3f794bdae664` + ); + expect(res.status).toBe(400); + expect(res.body.message).toBe(PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE); + }); + + it("Does not read with negative qnHistLimit", async () => { + const res = await request.get( + `${BASE_URL}?page=1&qnHistLimit=-10&userId=66f77e9f27ab3f794bdae664` + ); + expect(res.status).toBe(400); + expect(res.body.message).toBe(PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE); + }); + + it("Does not read with invalid userId format", async () => { + const res = await request.get( + `${BASE_URL}?page=1&qnHistLimit=10&userId=6` + ); + expect(res.status).toBe(400); + expect(res.body.message).toBe(MONGO_OBJ_ID_MALFORMED_MESSAGE); + }); + }); + + describe("GET /:id", () => { + it("Reads existing qn history", async () => { + const userIds = ["66f77e9f27ab3f794bdae664", "66f77e9f27ab3f794bdae665"]; + const questionId = "66f77e9f27ab3f794bdae666"; + const title = faker.lorem.lines(1); + const submissionStatus = "Attempted"; + const dateAttempted = new Date(); + const timeTaken = 0; + const language = "Python"; + const newQnHistory = new QnHistory({ + userIds, + questionId, + title, + submissionStatus, + dateAttempted, + timeTaken, + language, + }); + await newQnHistory.save(); + const res = await request.get(`${BASE_URL}/${newQnHistory.id}`); + expect(res.status).toBe(200); + expect(res.body.qnHistory.userIds).toEqual(userIds); + expect(res.body.qnHistory.questionId).toBe(questionId); + expect(res.body.qnHistory.title).toBe(title); + expect(res.body.qnHistory.submissionStatus).toBe(submissionStatus); + expect(res.body.qnHistory.dateAttempted).toBe( + dateAttempted.toISOString() + ); + expect(res.body.qnHistory.timeTaken).toBe(timeTaken); + expect(res.body.qnHistory.language).toBe(language); + }); + + it("Does not read non-existing qn history with invalid object id", async () => { + const res = await request.get(`${BASE_URL}/blah`); + expect(res.status).toBe(400); + expect(res.body.message).toBe(MONGO_OBJ_ID_MALFORMED_MESSAGE); + }); + + it("Does not read non-existing qn history with valid object id", async () => { + const res = await request.get(`${BASE_URL}/66f77e9f27ab3f794bdae664`); + expect(res.status).toBe(404); + expect(res.body.message).toBe(QN_HIST_NOT_FOUND_MESSAGE); + }); + }); + + describe("PUT /:id", () => { + it("Updates an existing qn history", async () => { + const userIds = ["66f77e9f27ab3f794bdae664", "66f77e9f27ab3f794bdae665"]; + const questionId = "66f77e9f27ab3f794bdae666"; + const title = faker.lorem.lines(1); + const submissionStatus = "Attempted"; + const dateAttempted = new Date(); + const timeTaken = 0; + const language = "Python"; + const newQnHistory = new QnHistory({ + userIds, + questionId, + title, + submissionStatus, + dateAttempted, + timeTaken, + language, + }); + await newQnHistory.save(); + + const updatedTitle = title.toUpperCase(); + const updatedQnHistory = { + userIds, + questionId, + title: updatedTitle, + submissionStatus, + dateAttempted, + timeTaken, + language, + }; + + const res = await request + .put(`${BASE_URL}/${newQnHistory.id}`) + .send(updatedQnHistory); + + expect(res.status).toBe(200); + expect(res.body.qnHistory.userIds).toEqual(userIds); + expect(res.body.qnHistory.questionId).toBe(questionId); + expect(res.body.qnHistory.title).toBe(updatedTitle); + expect(res.body.qnHistory.submissionStatus).toBe(submissionStatus); + expect(res.body.qnHistory.dateAttempted).toBe( + dateAttempted.toISOString() + ); + expect(res.body.qnHistory.timeTaken).toBe(timeTaken); + expect(res.body.qnHistory.language).toBe(language); + }); + + it("Does not update non-existing qn history with invalid object id", async () => { + const updatedQnHistory = { + title: "updatedTitle", + }; + const res = await request.put(`${BASE_URL}/blah`).send(updatedQnHistory); + + expect(res.status).toBe(400); + expect(res.body.message).toBe(MONGO_OBJ_ID_MALFORMED_MESSAGE); + }); + + it("Does not update non-existing qn history with valid object id", async () => { + const updatedQnHistory = { + title: "updatedTitle", + }; + const res = await request + .put(`${BASE_URL}/66f77e9f27ab3f794bdae664`) + .send(updatedQnHistory); + + expect(res.status).toBe(404); + expect(res.body.message).toBe(QN_HIST_NOT_FOUND_MESSAGE); + }); + }); + + describe("DELETE /:id", () => { + it("Deletes existing qn history", async () => { + const userIds = ["66f77e9f27ab3f794bdae664", "66f77e9f27ab3f794bdae665"]; + const questionId = "66f77e9f27ab3f794bdae666"; + const title = faker.lorem.lines(1); + const submissionStatus = "Attempted"; + const dateAttempted = new Date(); + const timeTaken = 0; + const language = "Python"; + const newQnHistory = new QnHistory({ + userIds, + questionId, + title, + submissionStatus, + dateAttempted, + timeTaken, + language, + }); + await newQnHistory.save(); + const res = await request.delete(`${BASE_URL}/${newQnHistory.id}`); + expect(res.status).toBe(200); + }); + + it("Does not delete non-existing qn history with invalid object id", async () => { + const res = await request.delete(`${BASE_URL}/blah`); + expect(res.status).toBe(400); + expect(res.body.message).toBe(MONGO_OBJ_ID_MALFORMED_MESSAGE); + }); + + it("Does not delete non-existing qn history with valid object id", async () => { + const res = await request.delete(`${BASE_URL}/66f77e9f27ab3f794bdae664`); + expect(res.status).toBe(404); + expect(res.body.message).toBe(QN_HIST_NOT_FOUND_MESSAGE); + }); + }); +}); diff --git a/backend/qn-history-service/tests/setup.ts b/backend/qn-history-service/tests/setup.ts new file mode 100644 index 0000000000..37d7edf1fa --- /dev/null +++ b/backend/qn-history-service/tests/setup.ts @@ -0,0 +1,25 @@ +import mongoose from "mongoose"; + +beforeAll(async () => { + const mongoUri = + process.env.MONGO_URI_TEST || "mongodb://mongo:mongo@test-mongo:27017/"; + + if (mongoose.connection.readyState !== 0) { + await mongoose.disconnect(); + } + + await mongoose.connect(mongoUri, {}); +}); + +afterEach(async () => { + const collections = await mongoose.connection.db?.collections(); + if (collections) { + for (const collection of collections) { + await collection.deleteMany({}); + } + } +}); + +afterAll(async () => { + await mongoose.connection.close(); +}); From d67572abbd99bc5a35b3f628de24e8133ea9215a Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sun, 3 Nov 2024 02:27:30 +0800 Subject: [PATCH 069/192] Switch to yjs --- backend/collab-service/package-lock.json | 68 ++++++++++++++++- backend/collab-service/package.json | 4 +- .../src/handlers/websocketHandler.ts | 74 +++++++++++++++++++ backend/collab-service/src/server.ts | 8 +- frontend/package-lock.json | 1 + frontend/package.json | 1 + frontend/src/components/CodeEditor/index.tsx | 74 +++++++++++-------- frontend/src/utils/collabCursor.ts | 14 +++- frontend/src/utils/collabSocket.ts | 46 +++++++++++- 9 files changed, 249 insertions(+), 41 deletions(-) diff --git a/backend/collab-service/package-lock.json b/backend/collab-service/package-lock.json index 5d09b825e0..009a4e8cca 100644 --- a/backend/collab-service/package-lock.json +++ b/backend/collab-service/package-lock.json @@ -19,7 +19,9 @@ "redis": "^4.7.0", "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", - "yaml": "^2.6.0" + "y-protocols": "^1.0.6", + "yaml": "^2.6.0", + "yjs": "^13.6.20" }, "devDependencies": { "@eslint/js": "^9.13.0", @@ -4657,6 +4659,15 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -5500,6 +5511,26 @@ "node": ">= 0.8.0" } }, + "node_modules/lib0": { + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", + "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7225,6 +7256,25 @@ } } }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7283,6 +7333,22 @@ "node": ">=12" } }, + "node_modules/yjs": { + "version": "13.6.20", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.20.tgz", + "integrity": "sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==", + "dependencies": { + "lib0": "^0.2.98" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index dc43147ab2..5529171a82 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -24,7 +24,9 @@ "redis": "^4.7.0", "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", - "yaml": "^2.6.0" + "y-protocols": "^1.0.6", + "yaml": "^2.6.0", + "yjs": "^13.6.20" }, "devDependencies": { "@eslint/js": "^9.13.0", diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 4f6a733ea6..b5784082ed 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -3,6 +3,7 @@ import { io } from "../server"; import redisClient from "../config/redis"; import { ChangeSet, Text } from "@codemirror/state"; import { rebaseUpdates, Update } from "@codemirror/collab"; +import * as Y from "yjs"; enum CollabEvents { // Receive @@ -38,6 +39,79 @@ interface CollabSession { const collabSessions = new Map(); +const yCollabSessions = new Map(); + +export const handleYWebsocketCollabEvents = (socket: Socket) => { + socket.on(CollabEvents.JOIN, async (roomId: string) => { + if (!roomId) { + return; + } + + const room = io.sockets.adapter.rooms.get(roomId); + if (room && room.size >= 2) { + socket.emit(CollabEvents.ROOM_FULL); + return; + } + + socket.join(roomId); + socket.data.roomId = roomId; + + // in case of disconnect, send the code to the user when he rejoins + // const collabSession = await redisClient.get(`collaboration:${roomId}`); + // if (collabSession) { + // if (!yCollabSessions.has(roomId)) { + // yCollabSessions.set(roomId, JSON.parse(collabSession) as Y.Doc); + // } + // } else { + // const ydoc = new Y.Doc(); + // yCollabSessions.set(roomId, ydoc); + // } + if (!yCollabSessions.has(roomId)) { + const ydoc = new Y.Doc(); + yCollabSessions.set(roomId, ydoc); + } + socket.emit("sync", Y.encodeStateAsUpdate(yCollabSessions.get(roomId)!)); + socket.emit(CollabEvents.USER_CONNECTED); + + // inform the other user that a new user has joined + socket.to(roomId).emit(CollabEvents.NEW_USER_CONNECTED); + }); + + socket.on("update", (roomId: string, update: Uint8Array) => { + let ydoc = yCollabSessions.get(roomId); + if (!ydoc) { + ydoc = new Y.Doc(); + } + Y.applyUpdate(ydoc, update); + + socket.to(roomId).emit("update", update); + }); + + socket.on( + "cursor_update", + ( + roomId: string, + cursor: { uid: string; username: string; from: number; to: number } + ) => { + socket.to(roomId).emit("cursor_update", cursor); + } + ); + + socket.on(CollabEvents.LEAVE, (roomId: string) => { + if (!roomId) { + return; + } + + socket.leave(roomId); + const room = io.sockets.adapter.rooms.get(roomId); + if (room?.size === 0) { + collabSessions.delete(roomId); + } else { + socket.to(roomId).emit(CollabEvents.PARTNER_LEFT); + } + }); +}; + export const handleWebsocketCollabEvents = (socket: Socket) => { socket.on(CollabEvents.JOIN, async (roomId: string) => { if (!roomId) { diff --git a/backend/collab-service/src/server.ts b/backend/collab-service/src/server.ts index c1d11c7333..ccfea342ab 100644 --- a/backend/collab-service/src/server.ts +++ b/backend/collab-service/src/server.ts @@ -1,6 +1,9 @@ import http from "http"; import app, { allowedOrigins } from "./app.ts"; -import { handleWebsocketCollabEvents } from "./handlers/websocketHandler.ts"; +import { + handleWebsocketCollabEvents, + handleYWebsocketCollabEvents, +} from "./handlers/websocketHandler.ts"; import { Server, Socket } from "socket.io"; import { connectRedis } from "./config/redis.ts"; @@ -14,7 +17,8 @@ export const io = new Server(server, { }); io.on("connection", (socket: Socket) => { - handleWebsocketCollabEvents(socket); + // handleWebsocketCollabEvents(socket); + handleYWebsocketCollabEvents(socket); }); const PORT = process.env.SERVICE_PORT || 3003; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d6ab181bd5..8bbbe4c62b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,6 +34,7 @@ "socket.io-client": "^4.8.0", "vite-plugin-svgr": "^4.2.0", "y-codemirror.next": "^0.3.5", + "y-protocols": "^1.0.6", "y-webrtc": "^10.3.0", "yjs": "^13.6.20" }, diff --git a/frontend/package.json b/frontend/package.json index 8629dd89f9..bdf32085d0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,6 +38,7 @@ "socket.io-client": "^4.8.0", "vite-plugin-svgr": "^4.2.0", "y-codemirror.next": "^0.3.5", + "y-protocols": "^1.0.6", "y-webrtc": "^10.3.0", "yjs": "^13.6.20" }, diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index b628f5cbe6..3cf4f694eb 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -5,12 +5,17 @@ import { EditorView } from "@codemirror/view"; import { EditorState } from "@codemirror/state"; import { useEffect, useState } from "react"; import { + awareness, getDocument, initDocument, peerExtension, + receiveCursorUpdates, + removeCursorListener, + ytext, } from "../../utils/collabSocket"; import Loader from "../Loader"; import { cursorExtension } from "../../utils/collabCursor"; +import { yCollab } from "y-codemirror.next"; interface CodeEditorProps { uid: string; @@ -48,40 +53,44 @@ const CodeEditor: React.FC = (props) => { }); useEffect(() => { - if (isReadOnly) { - setCodeEditorState({ - version: 0, - doc: template, - }); - return; - } + return () => removeCursorListener(); + }, []); - const fetchDocument = async () => { - if (!roomId) { - return; - } + // useEffect(() => { + // if (isReadOnly) { + // setCodeEditorState({ + // version: 0, + // doc: template, + // }); + // return; + // } - try { - if (template) { - await initDocument(roomId, template); - } + // const fetchDocument = async () => { + // if (!roomId) { + // return; + // } - const { version, doc } = await getDocument(roomId); - setCodeEditorState({ - version: version, - doc: doc.toString(), - }); - } catch (error) { - console.error("Error fetching document: ", error); - } - }; + // try { + // if (template) { + // await initDocument(roomId, template); + // } - fetchDocument(); - }, []); + // const { version, doc } = await getDocument(roomId); + // setCodeEditorState({ + // version: version, + // doc: doc.toString(), + // }); + // } catch (error) { + // console.error("Error fetching document: ", error); + // } + // }; + + // fetchDocument(); + // }, []); - if (codeEditorState.version === null || codeEditorState.doc === null) { - return ; - } + // if (codeEditorState.version === null || codeEditorState.doc === null) { + // return ; + // } return ( = (props) => { extensions={[ basicSetup(), languageSupport[language as keyof typeof languageSupport], - peerExtension(roomId, codeEditorState.version, uid), - cursorExtension(uid, username), + yCollab(ytext, awareness), + // peerExtension(roomId, codeEditorState.version, uid), + cursorExtension(roomId, uid, username), EditorView.editable.of(!isReadOnly), EditorState.readOnly.of(isReadOnly), ]} - value={codeEditorState.doc} + // value={codeEditorState.doc} /> ); }; diff --git a/frontend/src/utils/collabCursor.ts b/frontend/src/utils/collabCursor.ts index 5c29b1a5e9..b2e435b473 100644 --- a/frontend/src/utils/collabCursor.ts +++ b/frontend/src/utils/collabCursor.ts @@ -5,6 +5,7 @@ import { WidgetType, } from "@codemirror/view"; import { StateField, StateEffect } from "@codemirror/state"; +import { receiveCursorUpdates, sendCursorUpdates } from "./collabSocket"; // Adapted from https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets @@ -65,6 +66,7 @@ const cursorStateField = (uid: string): StateField => { for (const effect of transaction.effects) { // check for partner's cursor updates if (effect.is(updateCursor) && effect.value.uid !== uid) { + // if (effect.is(updateCursor)) { const cursorUpdates = []; if (effect.value.from !== effect.value.to) { @@ -128,7 +130,11 @@ const cursorBaseTheme = EditorView.baseTheme({ }, }); -export const cursorExtension = (uid: string, username: string) => { +export const cursorExtension = ( + roomId: string, + uid: string, + username: string +) => { return [ cursorStateField(uid), // handles cursor positions and highlights cursorBaseTheme, // provides cursor styling @@ -143,11 +149,11 @@ export const cursorExtension = (uid: string, username: string) => { to: transaction.selection.ranges[0].to, }; - update.view.dispatch({ - effects: updateCursor.of(cursor), - }); + sendCursorUpdates(roomId, cursor); } }); + + receiveCursorUpdates(update.view); }), ]; }; diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index c4958f7e2a..165b11784a 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -9,6 +9,8 @@ import { } from "@codemirror/collab"; import { io } from "socket.io-client"; import { updateCursor, Cursor } from "./collabCursor"; +import * as Y from "yjs"; +import { Awareness } from "y-protocols/awareness"; // Adapted from https://codemirror.net/examples/collab/ and https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets @@ -98,12 +100,34 @@ const pullUpdates = ( ); }; +export const ydoc = new Y.Doc(); +export const ytext = ydoc.getText("codemirror"); +export const awareness = new Awareness(ydoc); + export const join = (roomId: string): Promise => { collabSocket.connect(); collabSocket.emit(CollabEvents.JOIN, roomId); + // Listen for local document changes and send to the server + ydoc.on("update", (update) => { + collabSocket.emit("update", roomId, update); + }); + + // Listen for document updates from the server + collabSocket.on("update", (update) => { + Y.applyUpdate(ydoc, new Uint8Array(update)); + }); + return new Promise((resolve) => { - collabSocket.once(CollabEvents.USER_CONNECTED, () => resolve()); + // Listen for initial document state + collabSocket.once("sync", (update) => { + try { + Y.applyUpdate(ydoc, new Uint8Array(update)); + } catch (error) { + console.error("Sync initial state error: ", error); + } + resolve(); + }); }); }; @@ -112,6 +136,26 @@ export const leave = (roomId: string) => { collabSocket.disconnect(); }; +export const sendCursorUpdates = (roomId: string, cursor: Cursor) => { + collabSocket.emit("cursor_update", roomId, cursor); +}; + +export const receiveCursorUpdates = (view: EditorView) => { + if (collabSocket.hasListeners("cursor_update")) { + return; + } + + collabSocket.on("cursor_update", (cursor: Cursor) => { + view.dispatch({ + effects: updateCursor.of(cursor), + }); + }); +}; + +export const removeCursorListener = () => { + collabSocket.off("cursor_update"); +}; + export const initDocument = ( roomId: string, template: string From d4c86e9109c31e3d7f1d5f0b24b6deb8a67a5b80 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sun, 3 Nov 2024 13:36:11 +0800 Subject: [PATCH 070/192] Create ydoc on join --- .../src/handlers/websocketHandler.ts | 421 +++++++++--------- backend/collab-service/src/server.ts | 8 +- frontend/src/components/CodeEditor/index.tsx | 82 +--- frontend/src/pages/CollabSandbox/index.tsx | 17 +- frontend/src/utils/collabSocket.ts | 221 ++------- 5 files changed, 273 insertions(+), 476 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index b5784082ed..ef966ac2b9 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -1,9 +1,9 @@ import { Socket } from "socket.io"; import { io } from "../server"; import redisClient from "../config/redis"; -import { ChangeSet, Text } from "@codemirror/state"; -import { rebaseUpdates, Update } from "@codemirror/collab"; -import * as Y from "yjs"; +// import { ChangeSet, Text } from "@codemirror/state"; +// import { rebaseUpdates, Update } from "@codemirror/collab"; +import { Doc, Text, applyUpdate, encodeStateAsUpdate } from "yjs"; enum CollabEvents { // Receive @@ -11,6 +11,8 @@ enum CollabEvents { // CHANGE = "change", LEAVE = "leave", DISCONNECT = "disconnect", + UPDATE_REQUEST = "update_request", + UPDATE_CURSOR_REQUEST = "update_cursor_request", PUSH_UPDATES = "push_updates", PULL_UPDATES = "pull_updates", @@ -24,6 +26,9 @@ enum CollabEvents { // CODE_CHANGE = "code_change", PARTNER_LEFT = "partner_left", PARTNER_DISCONNECTED = "partner_disconnected", + SYNC = "sync", + UPDATE = "update", + UPDATE_CURSOR = "update_cursor", PULL_UPDATES_RESPONSE = "pull_updates_response", GET_DOCUMENT_RESPONSE = "get_document_response", @@ -31,17 +36,17 @@ enum CollabEvents { const EXPIRY_TIME = 3600; -interface CollabSession { - updates: Update[]; // updates.length = current version - doc: Text; - pendingPullUpdatesRequests: ((updates: Update[]) => void)[]; -} +// interface CollabSession { +// updates: Update[]; // updates.length = current version +// doc: Text; +// pendingPullUpdatesRequests: ((updates: Update[]) => void)[]; +// } -const collabSessions = new Map(); +// const collabSessions = new Map(); -const yCollabSessions = new Map(); +const collabSessions = new Map(); -export const handleYWebsocketCollabEvents = (socket: Socket) => { +export const handleWebsocketCollabEvents = (socket: Socket) => { socket.on(CollabEvents.JOIN, async (roomId: string) => { if (!roomId) { return; @@ -66,34 +71,40 @@ export const handleYWebsocketCollabEvents = (socket: Socket) => { // const ydoc = new Y.Doc(); // yCollabSessions.set(roomId, ydoc); // } - if (!yCollabSessions.has(roomId)) { - const ydoc = new Y.Doc(); - yCollabSessions.set(roomId, ydoc); + if (!collabSessions.has(roomId)) { + const doc = new Doc(); + collabSessions.set(roomId, doc); } - socket.emit("sync", Y.encodeStateAsUpdate(yCollabSessions.get(roomId)!)); + socket.emit( + CollabEvents.SYNC, + encodeStateAsUpdate(collabSessions.get(roomId)!) + ); socket.emit(CollabEvents.USER_CONNECTED); // inform the other user that a new user has joined socket.to(roomId).emit(CollabEvents.NEW_USER_CONNECTED); }); - socket.on("update", (roomId: string, update: Uint8Array) => { - let ydoc = yCollabSessions.get(roomId); - if (!ydoc) { - ydoc = new Y.Doc(); - } - Y.applyUpdate(ydoc, update); + socket.on( + CollabEvents.UPDATE_REQUEST, + (roomId: string, update: Uint8Array) => { + let doc = collabSessions.get(roomId); + if (!doc) { + doc = new Doc(); + } + applyUpdate(doc, update); - socket.to(roomId).emit("update", update); - }); + socket.to(roomId).emit(CollabEvents.UPDATE, update); + } + ); socket.on( - "cursor_update", + CollabEvents.UPDATE_CURSOR_REQUEST, ( roomId: string, cursor: { uid: string; username: string; from: number; to: number } ) => { - socket.to(roomId).emit("cursor_update", cursor); + socket.to(roomId).emit(CollabEvents.UPDATE_CURSOR, cursor); } ); @@ -112,183 +123,183 @@ export const handleYWebsocketCollabEvents = (socket: Socket) => { }); }; -export const handleWebsocketCollabEvents = (socket: Socket) => { - socket.on(CollabEvents.JOIN, async (roomId: string) => { - if (!roomId) { - return; - } - - const room = io.sockets.adapter.rooms.get(roomId); - if (room && room.size >= 2) { - socket.emit(CollabEvents.ROOM_FULL); - return; - } - - socket.join(roomId); - socket.data.roomId = roomId; - - // in case of disconnect, send the code to the user when he rejoins - const collabSession = await redisClient.get(`collaboration:${roomId}`); - if (collabSession) { - if (!collabSessions.has(roomId)) { - collabSessions.set(roomId, JSON.parse(collabSession) as CollabSession); - } - } else { - initCollabSession(roomId); - } - socket.emit(CollabEvents.USER_CONNECTED); - - // inform the other user that a new user has joined - socket.to(roomId).emit(CollabEvents.NEW_USER_CONNECTED); - }); - - // socket.on(CollabEvents.CHANGE, async (roomId: string, code: string) => { - // if (!roomId || !code) { - // return; - // } - - // await redisClient.set(`collaboration:${roomId}`, code, { - // EX: EXPIRY_TIME, - // }); - // socket.to(roomId).emit(CollabEvents.CODE_CHANGE, code); - // }); - - socket.on(CollabEvents.LEAVE, (roomId: string) => { - if (!roomId) { - return; - } - - socket.leave(roomId); - const room = io.sockets.adapter.rooms.get(roomId); - if (room?.size === 0) { - collabSessions.delete(roomId); - } else { - socket.to(roomId).emit(CollabEvents.PARTNER_LEFT); - } - }); - - socket.on(CollabEvents.DISCONNECT, () => { - const { roomId } = socket.data; - if (roomId) { - socket.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED); - } - }); - - handleCodeEditorEvents(socket); -}; - -/* Code Editor Events */ -// Adapted from https://codemirror.net/examples/collab/ and https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets - -const handleCodeEditorEvents = (socket: Socket) => { - socket.on( - CollabEvents.INIT_DOCUMENT, - (roomId: string, template: string, callback: () => void) => { - initCollabSession(roomId, template); - callback(); - } - ); - - socket.on(CollabEvents.GET_DOCUMENT, (roomId: string) => { - const { updates, doc } = initCollabSession(roomId); - socket.emit( - CollabEvents.GET_DOCUMENT_RESPONSE, - updates.length, - doc.toString() - ); - }); - - socket.on(CollabEvents.PULL_UPDATES, (roomId: string, version: number) => { - const { updates, pendingPullUpdatesRequests } = initCollabSession(roomId); - if (version < updates.length) { - // send the new updates - socket.emit( - CollabEvents.PULL_UPDATES_RESPONSE, - JSON.stringify(updates.slice(version)) - ); - } else { - // wait until there are new updates to send - pendingPullUpdatesRequests.push((updates) => { - socket.emit( - CollabEvents.PULL_UPDATES_RESPONSE, - JSON.stringify(updates.slice(version)) - ); - }); - } - }); - - // received new updates, notify any pending pullUpdates requests - socket.on( - CollabEvents.PUSH_UPDATES, - async ( - roomId: string, - version: number, - newUpdates: string, - callback: () => void - ) => { - const { updates, doc, pendingPullUpdatesRequests } = - initCollabSession(roomId); - let docUpdates = JSON.parse(newUpdates) as readonly Update[]; - - try { - // If the given version is the latest version, apply the new updates. - // Else, rebase updates first. - if (version < updates.length) { - docUpdates = rebaseUpdates(docUpdates, updates.slice(version)); - } - - for (const update of docUpdates) { - const changes = ChangeSet.fromJSON(update.changes); - updates.push({ - clientID: update.clientID, - changes: changes, - effects: update.effects, - }); - - const updatedCollabSession = { - updates: updates, - doc: changes.apply(doc), - pendingPullUpdatesRequests: pendingPullUpdatesRequests, - }; - collabSessions.set(roomId, updatedCollabSession); - - await redisClient.set( - `collaboration:${roomId}`, - JSON.stringify(updatedCollabSession), - { - EX: EXPIRY_TIME, - } - ); - } - callback(); - - while (pendingPullUpdatesRequests.length) { - pendingPullUpdatesRequests.pop()!(updates); - } - } catch (error) { - console.error(error); - callback(); - } - } - ); -}; - -const initCollabSession = ( - roomId: string, - template?: string -): CollabSession => { - const collabSession = collabSessions.get(roomId); - if (!collabSession) { - collabSessions.set(roomId, { - updates: [], - doc: Text.of([template ? template : ""]), - pendingPullUpdatesRequests: [], - }); - } else if (template) { - collabSessions.set(roomId, { - ...collabSession, - doc: Text.of([template]), - }); - } - return collabSessions.get(roomId)!; -}; +// export const handleWebsocketCollabEvents = (socket: Socket) => { +// socket.on(CollabEvents.JOIN, async (roomId: string) => { +// if (!roomId) { +// return; +// } + +// const room = io.sockets.adapter.rooms.get(roomId); +// if (room && room.size >= 2) { +// socket.emit(CollabEvents.ROOM_FULL); +// return; +// } + +// socket.join(roomId); +// socket.data.roomId = roomId; + +// // in case of disconnect, send the code to the user when he rejoins +// const collabSession = await redisClient.get(`collaboration:${roomId}`); +// if (collabSession) { +// if (!collabSessions.has(roomId)) { +// collabSessions.set(roomId, JSON.parse(collabSession) as CollabSession); +// } +// } else { +// initCollabSession(roomId); +// } +// socket.emit(CollabEvents.USER_CONNECTED); + +// // inform the other user that a new user has joined +// socket.to(roomId).emit(CollabEvents.NEW_USER_CONNECTED); +// }); + +// // socket.on(CollabEvents.CHANGE, async (roomId: string, code: string) => { +// // if (!roomId || !code) { +// // return; +// // } + +// // await redisClient.set(`collaboration:${roomId}`, code, { +// // EX: EXPIRY_TIME, +// // }); +// // socket.to(roomId).emit(CollabEvents.CODE_CHANGE, code); +// // }); + +// socket.on(CollabEvents.LEAVE, (roomId: string) => { +// if (!roomId) { +// return; +// } + +// socket.leave(roomId); +// const room = io.sockets.adapter.rooms.get(roomId); +// if (room?.size === 0) { +// collabSessions.delete(roomId); +// } else { +// socket.to(roomId).emit(CollabEvents.PARTNER_LEFT); +// } +// }); + +// socket.on(CollabEvents.DISCONNECT, () => { +// const { roomId } = socket.data; +// if (roomId) { +// socket.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED); +// } +// }); + +// handleCodeEditorEvents(socket); +// }; + +// /* Code Editor Events */ +// // Adapted from https://codemirror.net/examples/collab/ and https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets + +// const handleCodeEditorEvents = (socket: Socket) => { +// socket.on( +// CollabEvents.INIT_DOCUMENT, +// (roomId: string, template: string, callback: () => void) => { +// initCollabSession(roomId, template); +// callback(); +// } +// ); + +// socket.on(CollabEvents.GET_DOCUMENT, (roomId: string) => { +// const { updates, doc } = initCollabSession(roomId); +// socket.emit( +// CollabEvents.GET_DOCUMENT_RESPONSE, +// updates.length, +// doc.toString() +// ); +// }); + +// socket.on(CollabEvents.PULL_UPDATES, (roomId: string, version: number) => { +// const { updates, pendingPullUpdatesRequests } = initCollabSession(roomId); +// if (version < updates.length) { +// // send the new updates +// socket.emit( +// CollabEvents.PULL_UPDATES_RESPONSE, +// JSON.stringify(updates.slice(version)) +// ); +// } else { +// // wait until there are new updates to send +// pendingPullUpdatesRequests.push((updates) => { +// socket.emit( +// CollabEvents.PULL_UPDATES_RESPONSE, +// JSON.stringify(updates.slice(version)) +// ); +// }); +// } +// }); + +// // received new updates, notify any pending pullUpdates requests +// socket.on( +// CollabEvents.PUSH_UPDATES, +// async ( +// roomId: string, +// version: number, +// newUpdates: string, +// callback: () => void +// ) => { +// const { updates, doc, pendingPullUpdatesRequests } = +// initCollabSession(roomId); +// let docUpdates = JSON.parse(newUpdates) as readonly Update[]; + +// try { +// // If the given version is the latest version, apply the new updates. +// // Else, rebase updates first. +// if (version < updates.length) { +// docUpdates = rebaseUpdates(docUpdates, updates.slice(version)); +// } + +// for (const update of docUpdates) { +// const changes = ChangeSet.fromJSON(update.changes); +// updates.push({ +// clientID: update.clientID, +// changes: changes, +// effects: update.effects, +// }); + +// const updatedCollabSession = { +// updates: updates, +// doc: changes.apply(doc), +// pendingPullUpdatesRequests: pendingPullUpdatesRequests, +// }; +// collabSessions.set(roomId, updatedCollabSession); + +// await redisClient.set( +// `collaboration:${roomId}`, +// JSON.stringify(updatedCollabSession), +// { +// EX: EXPIRY_TIME, +// } +// ); +// } +// callback(); + +// while (pendingPullUpdatesRequests.length) { +// pendingPullUpdatesRequests.pop()!(updates); +// } +// } catch (error) { +// console.error(error); +// callback(); +// } +// } +// ); +// }; + +// const initCollabSession = ( +// roomId: string, +// template?: string +// ): CollabSession => { +// const collabSession = collabSessions.get(roomId); +// if (!collabSession) { +// collabSessions.set(roomId, { +// updates: [], +// doc: Text.of([template ? template : ""]), +// pendingPullUpdatesRequests: [], +// }); +// } else if (template) { +// collabSessions.set(roomId, { +// ...collabSession, +// doc: Text.of([template]), +// }); +// } +// return collabSessions.get(roomId)!; +// }; diff --git a/backend/collab-service/src/server.ts b/backend/collab-service/src/server.ts index ccfea342ab..c1d11c7333 100644 --- a/backend/collab-service/src/server.ts +++ b/backend/collab-service/src/server.ts @@ -1,9 +1,6 @@ import http from "http"; import app, { allowedOrigins } from "./app.ts"; -import { - handleWebsocketCollabEvents, - handleYWebsocketCollabEvents, -} from "./handlers/websocketHandler.ts"; +import { handleWebsocketCollabEvents } from "./handlers/websocketHandler.ts"; import { Server, Socket } from "socket.io"; import { connectRedis } from "./config/redis.ts"; @@ -17,8 +14,7 @@ export const io = new Server(server, { }); io.on("connection", (socket: Socket) => { - // handleWebsocketCollabEvents(socket); - handleYWebsocketCollabEvents(socket); + handleWebsocketCollabEvents(socket); }); const PORT = process.env.SERVICE_PORT || 3003; diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 3cf4f694eb..df1e97838d 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -3,34 +3,23 @@ import { langs } from "@uiw/codemirror-extensions-langs"; import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; import { EditorView } from "@codemirror/view"; import { EditorState } from "@codemirror/state"; -import { useEffect, useState } from "react"; -import { - awareness, - getDocument, - initDocument, - peerExtension, - receiveCursorUpdates, - removeCursorListener, - ytext, -} from "../../utils/collabSocket"; -import Loader from "../Loader"; +import { useEffect } from "react"; +import { removeCursorListener } from "../../utils/collabSocket"; import { cursorExtension } from "../../utils/collabCursor"; import { yCollab } from "y-codemirror.next"; +import { Text } from "yjs"; +import { Awareness } from "y-protocols/awareness"; interface CodeEditorProps { - uid: string; - username: string; + editorState?: { text: Text; awareness: Awareness }; + uid?: string; + username?: string; language: string; template?: string; roomId?: string; isReadOnly?: boolean; } -type CodeEditorState = { - version: number | null; - doc: string | null; -}; - const languageSupport = { Python: langs.python(), Java: langs.java(), @@ -39,59 +28,19 @@ const languageSupport = { const CodeEditor: React.FC = (props) => { const { - uid, - username, + editorState, + uid = "", + username = "", language, template = "", roomId = "", isReadOnly = false, } = props; - const [codeEditorState, setCodeEditorState] = useState({ - version: null, - doc: null, - }); - useEffect(() => { return () => removeCursorListener(); }, []); - // useEffect(() => { - // if (isReadOnly) { - // setCodeEditorState({ - // version: 0, - // doc: template, - // }); - // return; - // } - - // const fetchDocument = async () => { - // if (!roomId) { - // return; - // } - - // try { - // if (template) { - // await initDocument(roomId, template); - // } - - // const { version, doc } = await getDocument(roomId); - // setCodeEditorState({ - // version: version, - // doc: doc.toString(), - // }); - // } catch (error) { - // console.error("Error fetching document: ", error); - // } - // }; - - // fetchDocument(); - // }, []); - - // if (codeEditorState.version === null || codeEditorState.doc === null) { - // return ; - // } - return ( = (props) => { extensions={[ basicSetup(), languageSupport[language as keyof typeof languageSupport], - yCollab(ytext, awareness), - // peerExtension(roomId, codeEditorState.version, uid), - cursorExtension(roomId, uid, username), + ...(!isReadOnly && editorState + ? [ + yCollab(editorState.text, editorState.awareness), + cursorExtension(roomId, uid, username), + ] + : []), EditorView.editable.of(!isReadOnly), EditorState.readOnly.of(isReadOnly), ]} - // value={codeEditorState.doc} + value={isReadOnly ? template : undefined} /> ); }; diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 268f677f0b..690118c329 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -22,10 +22,16 @@ import QuestionDetailComponent from "../../components/QuestionDetail"; import { Navigate } from "react-router-dom"; import CodeEditor from "../../components/CodeEditor"; import { join, leave } from "../../utils/collabSocket"; +import { Text } from "yjs"; +import { Awareness } from "y-protocols/awareness"; const CollabSandbox: React.FC = () => { - const [connected, setConnected] = useState(false); + // const [connected, setConnected] = useState(false); const [showErrorScreen, setShowErrorScreen] = useState(false); + const [editorState, setEditorState] = useState<{ + text: Text; + awareness: Awareness; + } | null>(null); const match = useMatch(); if (!match) { @@ -59,14 +65,14 @@ const CollabSandbox: React.FC = () => { } getQuestionById(questionId, dispatch); - if (!matchId || connected) { + if (!matchId || editorState) { return; } const connectToCollabSession = async () => { try { - await join(matchId); - setConnected(true); + const { text, awareness } = await join(matchId); + setEditorState({ text, awareness }); } catch (error) { console.error("Error connecting to collab session: ", error); } @@ -110,7 +116,7 @@ const CollabSandbox: React.FC = () => { ); } - if (!selectedQuestion || !connected) { + if (!selectedQuestion || !editorState) { return ; } @@ -185,6 +191,7 @@ const CollabSandbox: React.FC = () => { })} > => { - const updates = fullUpdates.map((update) => ({ - clientID: update.clientID, // client who made the update - changes: update.changes.toJSON(), // document updates - effects: update.effects, // cursor updates - })); - - return new Promise((resolve) => { - collabSocket.emit( - CollabEvents.PUSH_UPDATES, - roomId, - version, - JSON.stringify(updates), - () => resolve() - ); - }); -}; - -const pullUpdates = ( - roomId: string, - version: number -): Promise => { - return new Promise((resolve) => { - collabSocket.emit(CollabEvents.PULL_UPDATES, roomId, version); - - collabSocket.once(CollabEvents.PULL_UPDATES_RESPONSE, (updates: string) => { - resolve(JSON.parse(updates)); - }); - }).then((updates) => - updates.map((update) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const effects: StateEffect[] = []; - - update.effects?.forEach((effect) => { - if ( - effect.value.uid && - effect.value.username && - effect.value.from && - effect.value.to - ) { - const cursor: Cursor = { - uid: effect.value.uid, - username: effect.value.username, - from: effect.value.from, - to: effect.value.to, - }; - effects.push(updateCursor.of(cursor)); - } - }); - - return { - clientID: update.clientID, - changes: ChangeSet.fromJSON(update.changes), - effects: effects, - }; - }) - ); -}; - -export const ydoc = new Y.Doc(); -export const ytext = ydoc.getText("codemirror"); -export const awareness = new Awareness(ydoc); - -export const join = (roomId: string): Promise => { +export const join = ( + roomId: string +): Promise<{ text: Text; awareness: Awareness }> => { collabSocket.connect(); collabSocket.emit(CollabEvents.JOIN, roomId); - // Listen for local document changes and send to the server - ydoc.on("update", (update) => { - collabSocket.emit("update", roomId, update); + const doc = new Doc(); + const text = doc.getText("codemirror"); + const awareness = new Awareness(doc); + + doc.on(CollabEvents.UPDATE, (update) => { + collabSocket.emit(CollabEvents.UPDATE_REQUEST, roomId, update); }); - // Listen for document updates from the server - collabSocket.on("update", (update) => { - Y.applyUpdate(ydoc, new Uint8Array(update)); + collabSocket.on(CollabEvents.UPDATE, (update) => { + applyUpdate(doc, new Uint8Array(update)); }); return new Promise((resolve) => { - // Listen for initial document state - collabSocket.once("sync", (update) => { - try { - Y.applyUpdate(ydoc, new Uint8Array(update)); - } catch (error) { - console.error("Sync initial state error: ", error); - } - resolve(); + collabSocket.once(CollabEvents.SYNC, (update) => { + applyUpdate(doc, new Uint8Array(update)); + resolve({ text: text, awareness: awareness }); }); }); }; @@ -137,15 +59,15 @@ export const leave = (roomId: string) => { }; export const sendCursorUpdates = (roomId: string, cursor: Cursor) => { - collabSocket.emit("cursor_update", roomId, cursor); + collabSocket.emit(CollabEvents.UPDATE_CURSOR_REQUEST, roomId, cursor); }; export const receiveCursorUpdates = (view: EditorView) => { - if (collabSocket.hasListeners("cursor_update")) { + if (collabSocket.hasListeners(CollabEvents.UPDATE_CURSOR)) { return; } - collabSocket.on("cursor_update", (cursor: Cursor) => { + collabSocket.on(CollabEvents.UPDATE_CURSOR, (cursor: Cursor) => { view.dispatch({ effects: updateCursor.of(cursor), }); @@ -153,96 +75,5 @@ export const receiveCursorUpdates = (view: EditorView) => { }; export const removeCursorListener = () => { - collabSocket.off("cursor_update"); -}; - -export const initDocument = ( - roomId: string, - template: string -): Promise => { - return new Promise((resolve) => { - collabSocket.emit(CollabEvents.INIT_DOCUMENT, roomId, template, () => - resolve() - ); - }); -}; - -export const getDocument = ( - roomId: string -): Promise<{ version: number; doc: Text }> => { - return new Promise((resolve) => { - collabSocket.emit(CollabEvents.GET_DOCUMENT, roomId); - - collabSocket.once( - CollabEvents.GET_DOCUMENT_RESPONSE, - (version: number, doc: string) => { - resolve({ - version: version, - doc: Text.of(doc.split("\n")), - }); - } - ); - }); -}; - -// handles push and pull updates -export const peerExtension = ( - roomId: string, - startVersion: number, - uid: string -) => { - const plugin = ViewPlugin.fromClass( - class { - private pushingUpdates = false; // to ensure only one running push request - private pullUpdates = true; - - constructor(private view: EditorView) { - this.pull(); - } - - update(update: ViewUpdate) { - if (update.docChanged || update.transactions.length) { - this.push(); - } - } - - async push() { - const updates = sendableUpdates(this.view.state); - if (this.pushingUpdates || !updates.length) { - return; - } - this.pushingUpdates = true; - const version = getSyncedVersion(this.view.state); - await pushUpdates(roomId, version, updates); - this.pushingUpdates = false; - - // check if there are still updates to push (failed / new updates) - if (sendableUpdates(this.view.state).length) { - setTimeout(() => this.push(), 100); - } - } - - async pull() { - while (this.pullUpdates) { - const version = getSyncedVersion(this.view.state); - const updates = await pullUpdates(roomId, version); // returns only if there are updates - this.view.dispatch(receiveUpdates(this.view.state, updates)); - } - } - - destroy() { - this.pullUpdates = false; - } - } - ); - - return [ - collab({ - startVersion: startVersion, - clientID: uid, - sharedEffects: (transaction) => - transaction.effects.filter((effect) => effect.is(updateCursor)), - }), - plugin, - ]; + collabSocket.off(CollabEvents.UPDATE_CURSOR); }; From 9fe7636002d8cdf7667354626cada5b9803bf90f Mon Sep 17 00:00:00 2001 From: jolynloh Date: Sun, 3 Nov 2024 14:54:40 +0800 Subject: [PATCH 071/192] Include test cases and code templates in question update --- .../src/controllers/questionController.ts | 61 ++++++++++------ frontend/src/pages/QuestionEdit/index.tsx | 73 +++++++++++++++++-- frontend/src/reducers/questionReducer.ts | 72 +++++++++++++++--- frontend/src/utils/validators.ts | 15 ++++ 4 files changed, 185 insertions(+), 36 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index ac0e450560..a570bc64e3 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -25,6 +25,10 @@ import { uploadFileToFirebase } from "../utils/utils"; import { QnListSearchFilterParams, RandomQnCriteria } from "../utils/types.ts"; const FIREBASE_TESTCASE_FILES_FOLDER_NAME = "testcaseFiles/"; +enum TestcaseFilesUploadRequestTypes { + CREATE = "create", + UPDATE = "update", +} export const createQuestion = async ( req: Request, @@ -131,39 +135,51 @@ export const createFileLink = async ( }); } + const isQuestionCreation = + req.body.requestType === TestcaseFilesUploadRequestTypes.CREATE; const tcFiles = req.files as { testcaseInputFile?: Express.Multer.File[]; testcaseOutputFile?: Express.Multer.File[]; }; - if (!tcFiles || !tcFiles.testcaseInputFile || !tcFiles.testcaseOutputFile) { + if ( + isQuestionCreation && + (!tcFiles || !tcFiles.testcaseInputFile || !tcFiles.testcaseOutputFile) + ) { return res .status(400) .json({ message: "Missing one or both testcase file(s)" }); } try { - const testcaseInputFile = tcFiles - .testcaseInputFile[0] as Express.Multer.File; - const testcaseOutputFile = tcFiles - .testcaseOutputFile[0] as Express.Multer.File; - - const [tcInputFileUrl, tcOutputFileUrl] = await Promise.all([ - uploadFileToFirebase( - testcaseInputFile, - FIREBASE_TESTCASE_FILES_FOLDER_NAME, - ), - uploadFileToFirebase( - testcaseOutputFile, - FIREBASE_TESTCASE_FILES_FOLDER_NAME, - ), - ]); + const uploadPromises = []; + + if (tcFiles.testcaseInputFile) { + const inputFile = tcFiles.testcaseInputFile[0] as Express.Multer.File; + uploadPromises.push( + uploadFileToFirebase(inputFile, FIREBASE_TESTCASE_FILES_FOLDER_NAME), + ); + } else { + uploadPromises.push(Promise.resolve(null)); + } + + if (tcFiles.testcaseOutputFile) { + const outputFile = tcFiles.testcaseOutputFile[0] as Express.Multer.File; + uploadPromises.push( + uploadFileToFirebase(outputFile, FIREBASE_TESTCASE_FILES_FOLDER_NAME), + ); + } else { + uploadPromises.push(Promise.resolve(null)); + } + + const [tcInputFileUrl, tcOutputFileUrl] = + await Promise.all(uploadPromises); return res.status(200).json({ message: "Files uploaded successfully", urls: { - testcaseInputFileUrl: tcInputFileUrl, - testcaseOutputFileUrl: tcOutputFileUrl, + testcaseInputFileUrl: tcInputFileUrl || "", + testcaseOutputFileUrl: tcOutputFileUrl || "", }, }); } catch (error) { @@ -218,9 +234,9 @@ export const updateQuestion = async ( const updatedQuestionTemplate = await QuestionTemplate.findOneAndUpdate( { questionId: id }, { - ...(pythonTemplate !== undefined && { pythonTemplate }), - ...(javaTemplate !== undefined && { javaTemplate }), - ...(cTemplate !== undefined && { cTemplate }), + pythonTemplate, + javaTemplate, + cTemplate, }, { new: true }, ); @@ -418,6 +434,9 @@ const formatQuestionIndivResponse = ( description: question.description, complexity: question.complexity, categories: question.category, + testcases: question.testcases, + testcaseInputFileUrl: question.testcaseInputFileUrl, + testcaseOutputFileUrl: question.testcaseOutputFileUrl, pythonTemplate: questionTemplate ? questionTemplate.pythonTemplate : "", javaTemplate: questionTemplate ? questionTemplate.javaTemplate : "", cTemplate: questionTemplate ? questionTemplate.cTemplate : "", diff --git a/frontend/src/pages/QuestionEdit/index.tsx b/frontend/src/pages/QuestionEdit/index.tsx index b522c5144f..b0bd509165 100644 --- a/frontend/src/pages/QuestionEdit/index.tsx +++ b/frontend/src/pages/QuestionEdit/index.tsx @@ -28,6 +28,12 @@ import QuestionMarkdown from "../../components/QuestionMarkdown"; import QuestionImageContainer from "../../components/QuestionImageContainer"; import QuestionCategoryAutoComplete from "../../components/QuestionCategoryAutoComplete"; import QuestionDetail from "../../components/QuestionDetail"; +import QuestionTestCases, { + TestCase, +} from "../../components/QuestionTestCases"; +import QuestionTestCasesFileUpload from "../../components/QuestionTestCasesFileUpload"; +import QuestionCodeTemplates from "../../components/QuestionCodeTemplates"; +import { isTestcaseUnchanged } from "../../utils/validators"; const QuestionEdit = () => { const navigate = useNavigate(); @@ -37,10 +43,23 @@ const QuestionEdit = () => { const [title, setTitle] = useState(""); const [markdownText, setMarkdownText] = useState(""); - const [selectedComplexity, setselectedComplexity] = useState( + const [selectedComplexity, setSelectedComplexity] = useState( null ); const [selectedCategories, setSelectedCategories] = useState([]); + const [testCases, setTestCases] = useState([]); + const [testcaseInputFile, setTestcaseInputFile] = useState(null); + const [testcaseOutputFile, setTestcaseOutputFile] = useState( + null + ); + + const [codeTemplates, setCodeTemplates] = useState<{ [key: string]: string }>( + { + python: "", + java: "", + c: "", + } + ); const [uploadedImagesUrl, setUploadedImagesUrl] = useState([]); const [isPreviewQuestion, setIsPreviewQuestion] = useState(false); @@ -56,8 +75,14 @@ const QuestionEdit = () => { if (state.selectedQuestion) { setTitle(state.selectedQuestion.title); setMarkdownText(state.selectedQuestion.description); - setselectedComplexity(state.selectedQuestion.complexity); + setSelectedComplexity(state.selectedQuestion.complexity); setSelectedCategories(state.selectedQuestion.categories); + setTestCases(state.selectedQuestion.testcases); + setCodeTemplates({ + python: state.selectedQuestion.pythonTemplate, + java: state.selectedQuestion.javaTemplate, + c: state.selectedQuestion.cTemplate, + }); } }, [state.selectedQuestion]); @@ -77,7 +102,13 @@ const QuestionEdit = () => { title === state.selectedQuestion.title && markdownText === state.selectedQuestion.description && selectedComplexity === state.selectedQuestion.complexity && - selectedCategories === state.selectedQuestion.categories + selectedCategories === state.selectedQuestion.categories && + isTestcaseUnchanged(testCases, state.selectedQuestion.testcases) && + codeTemplates.python === state.selectedQuestion.pythonTemplate && + codeTemplates.java === state.selectedQuestion.javaTemplate && + codeTemplates.c === state.selectedQuestion.cTemplate && + testcaseInputFile === null && + testcaseOutputFile === null ) { toast.error(NO_QUESTION_CHANGES); return; @@ -87,7 +118,12 @@ const QuestionEdit = () => { !title || !markdownText || !selectedComplexity || - selectedCategories.length === 0 + selectedCategories.length === 0 || + testCases.some( + (testCase) => + testCase.input.trim() === "" || testCase.expectedOutput.trim() === "" + ) || + Object.values(codeTemplates).some((value) => value === "") ) { toast.error(FILL_ALL_FIELDS); return; @@ -100,6 +136,16 @@ const QuestionEdit = () => { description: markdownText, complexity: selectedComplexity, categories: selectedCategories, + testcases: testCases, + testcaseInputFileUrl: state.selectedQuestion.testcaseInputFileUrl, + testcaseOutputFileUrl: state.selectedQuestion.testcaseOutputFileUrl, + pythonTemplate: codeTemplates.python, + javaTemplate: codeTemplates.java, + cTemplate: codeTemplates.c, + }, + { + testcaseInputFile: testcaseInputFile, + testcaseOutputFile: testcaseOutputFile, }, dispatch ); @@ -142,7 +188,7 @@ const QuestionEdit = () => { sx={{ marginTop: 2 }} value={selectedComplexity} onChange={(_e, newcomplexitySelected) => { - setselectedComplexity(newcomplexitySelected); + setSelectedComplexity(newcomplexitySelected); }} renderInput={(params) => ( @@ -163,6 +209,23 @@ const QuestionEdit = () => { markdownText={markdownText} setMarkdownText={setMarkdownText} /> + + + + + + )} diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts index 1eb959d46b..1b4dec6b29 100644 --- a/frontend/src/reducers/questionReducer.ts +++ b/frontend/src/reducers/questionReducer.ts @@ -3,8 +3,8 @@ import { questionClient } from "../utils/api"; import { isString, isStringArray } from "../utils/typeChecker"; type TestcaseFiles = { - testcaseInputFile: File; - testcaseOutputFile: File; + testcaseInputFile: File | null; + testcaseOutputFile: File | null; }; type Testcases = { @@ -13,6 +13,11 @@ type Testcases = { expectedOutput: string; }; +export const enum TestcaseFilesUploadRequestTypes { + CREATE = "create", + UPDATE = "update", +} + type QuestionDetail = { id: string; title: string; @@ -25,6 +30,11 @@ type QuestionDetail = { cTemplate: string; }; +type QuestionDetailWithUrl = QuestionDetail & { + testcaseInputFileUrl: string; + testcaseOutputFileUrl: string; +}; + type QuestionListDetail = { id: string; title: string; @@ -53,21 +63,26 @@ enum QuestionActionTypes { type QuestionActions = { type: QuestionActionTypes; - payload: QuestionList | QuestionDetail | string[] | string; + payload: + | QuestionList + | QuestionDetail + | QuestionDetailWithUrl + | string[] + | string; }; type QuestionsState = { questionCategories: Array; questions: Array; questionCount: number; - selectedQuestion: QuestionDetail | null; + selectedQuestion: QuestionDetailWithUrl | null; questionCategoriesError: string | null; questionListError: string | null; selectedQuestionError: string | null; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const isQuestion = (question: any): question is QuestionDetail => { +const isQuestion = (question: any): question is QuestionDetailWithUrl => { if (!question || typeof question !== "object") { return false; } @@ -106,7 +121,8 @@ export const initialState: QuestionsState = { }; export const uploadTestcaseFiles = async ( - data: TestcaseFiles + data: TestcaseFiles, + requestType: TestcaseFilesUploadRequestTypes ): Promise<{ message: string; urls: { @@ -115,8 +131,9 @@ export const uploadTestcaseFiles = async ( }; } | null> => { const formData = new FormData(); - formData.append("testcaseInputFile", data.testcaseInputFile); - formData.append("testcaseOutputFile", data.testcaseOutputFile); + formData.append("testcaseInputFile", data.testcaseInputFile ?? ""); + formData.append("testcaseOutputFile", data.testcaseOutputFile ?? ""); + formData.append("requestType", requestType); try { const accessToken = localStorage.getItem("token"); @@ -137,7 +154,10 @@ export const createQuestion = async ( testcaseFiles: TestcaseFiles, dispatch: Dispatch ): Promise => { - const uploadResult = await uploadTestcaseFiles(testcaseFiles); + const uploadResult = await uploadTestcaseFiles( + testcaseFiles, + TestcaseFilesUploadRequestTypes.CREATE + ); if (!uploadResult) { dispatch({ @@ -260,9 +280,34 @@ export const getQuestionById = ( export const updateQuestionById = async ( questionId: string, - question: Omit, + question: Omit, + testcaseFiles: TestcaseFiles, dispatch: Dispatch ): Promise => { + let urls = {}; + + if (Object.values(testcaseFiles).some((file) => file !== null)) { + const uploadResult = await uploadTestcaseFiles( + testcaseFiles, + TestcaseFilesUploadRequestTypes.UPDATE + ); + + if (!uploadResult) { + dispatch({ + type: QuestionActionTypes.ERROR_CREATING_QUESTION, + payload: "Failed to upload test case file(s).", + }); + return false; + } + + const { testcaseInputFileUrl, testcaseOutputFileUrl } = uploadResult.urls; + + urls = { + ...(testcaseInputFileUrl ? { testcaseInputFileUrl } : {}), + ...(testcaseOutputFileUrl ? { testcaseOutputFileUrl } : {}), + }; + } + const accessToken = localStorage.getItem("token"); return questionClient .put( @@ -272,6 +317,13 @@ export const updateQuestionById = async ( description: question.description, complexity: question.complexity, category: question.categories, + testcases: question.testcases, + testcaseInputFileUrl: question.testcaseInputFileUrl, + testcaseOutputFileUrl: question.testcaseOutputFileUrl, + ...urls, + pythonTemplate: question.pythonTemplate, + javaTemplate: question.javaTemplate, + cTemplate: question.cTemplate, }, { headers: { diff --git a/frontend/src/utils/validators.ts b/frontend/src/utils/validators.ts index bec4fdab48..9db98f55d9 100644 --- a/frontend/src/utils/validators.ts +++ b/frontend/src/utils/validators.ts @@ -1,5 +1,6 @@ /* eslint-disable */ +import { TestCase } from "../components/QuestionTestCases"; import { BIO_MAX_LENGTH_ERROR_MESSAGE, EMAIL_INVALID_ERROR_MESSAGE, @@ -119,3 +120,17 @@ export const passwordValidators = [ message: PASSWORD_SPECIAL_CHAR_ERROR_MESSAGE, }, ]; + +export const isTestcaseUnchanged = ( + newTc: Array, + oldTc: Array +) => { + return ( + newTc.length === oldTc.length && + newTc.every( + (tc, i) => + tc.input === oldTc[i].input && + tc.expectedOutput === oldTc[i].expectedOutput + ) + ); +}; From 9d7fd6743fa9b7292f29ba7f779b14b5e096995c Mon Sep 17 00:00:00 2001 From: jolynloh Date: Sun, 3 Nov 2024 15:14:27 +0800 Subject: [PATCH 072/192] Add default code templates --- .../src/controllers/questionController.ts | 2 ++ frontend/src/pages/NewQuestion/index.tsx | 9 ++++++--- frontend/src/utils/constants.ts | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index a570bc64e3..f46df1183b 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -25,6 +25,7 @@ import { uploadFileToFirebase } from "../utils/utils"; import { QnListSearchFilterParams, RandomQnCriteria } from "../utils/types.ts"; const FIREBASE_TESTCASE_FILES_FOLDER_NAME = "testcaseFiles/"; + enum TestcaseFilesUploadRequestTypes { CREATE = "create", UPDATE = "update", @@ -137,6 +138,7 @@ export const createFileLink = async ( const isQuestionCreation = req.body.requestType === TestcaseFilesUploadRequestTypes.CREATE; + const tcFiles = req.files as { testcaseInputFile?: Express.Multer.File[]; testcaseOutputFile?: Express.Multer.File[]; diff --git a/frontend/src/pages/NewQuestion/index.tsx b/frontend/src/pages/NewQuestion/index.tsx index 2b000fe350..afbc801541 100644 --- a/frontend/src/pages/NewQuestion/index.tsx +++ b/frontend/src/pages/NewQuestion/index.tsx @@ -16,9 +16,12 @@ import { toast } from "react-toastify"; import { ABORT_CREATE_OR_EDIT_QUESTION_CONFIRMATION_MESSAGE, + C_CODE_TEMPLATE, complexityList, FAILED_QUESTION_CREATE, FILL_ALL_FIELDS, + JAVA_CODE_TEMPLATE, + PYTHON_CODE_TEMPLATE, SUCCESS_QUESTION_CREATE, } from "../../utils/constants"; import AppMargin from "../../components/AppMargin"; @@ -57,9 +60,9 @@ const NewQuestion = () => { const [codeTemplates, setCodeTemplates] = useState<{ [key: string]: string }>( { - python: "", - java: "", - c: "", + python: PYTHON_CODE_TEMPLATE, + java: JAVA_CODE_TEMPLATE, + c: C_CODE_TEMPLATE, } ); diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 2163a4b92b..eacb2e5b2e 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -118,3 +118,8 @@ export const COLLABORATIVE_EDITOR_PATH = "/collaborative_editor.png"; export const ADD_QUESTION_TEST_CASE_TOOLTIP_MESSAGE = `Add at least 1 and at most 3 test cases.
This will be displayed to users.`; export const ADD_TEST_CASE_FILES_TOOLTIP_MESSAGE = `Upload files for executing test cases backend when user submits code.

This is a required field.
Only text files accepted.`; export const CODE_TEMPLATES_TOOLTIP_MESSAGE = `This is a required field.
Fill in a code template for each language.`; + +/* Code Templates */ +export const PYTHON_CODE_TEMPLATE = `# Please do not modify the main function\ndef main():\n\tprint(convert_to_string_format(solution()))\n\n\n# Write your code here\ndef solution():\n\treturn None\n\n\nif __name__ == "__main__":\n\tmain()\n`; +export const JAVA_CODE_TEMPLATE = `public class Main {\n\t// Please do not modify the main function\n\tpublic static void main(String[] args) {\n\t\tSystem.out.println(convert_to_string_format(solution()));\n\t}\n\n\t// Write your code here and return the appropriate type\n\tpublic static String solution() {\n\t\treturn null;\n\t}\n}\n`; +export const C_CODE_TEMPLATE = `#include \n\n// Write your code here and return the appropriate type\nconst char* solution() {\n\treturn "";\n}\n\n// Please do not modify the main function\nint main() {\n\tprintf("%s\\n", convert_to_string_format(solution()));\n\treturn 0;\n}\n`; From 64b95f137d6f91bc13ea990a4aa292b1c52900c8 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Sun, 3 Nov 2024 16:33:55 +0800 Subject: [PATCH 073/192] Update read random question controller --- .../src/controllers/questionController.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index f46df1183b..7dca447576 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -384,9 +384,18 @@ export const readRandomQuestion = async ( return; } + const chosenQuestion = randomQuestion[0]; + + const questionTemplate = await QuestionTemplate.findOne({ + questionId: chosenQuestion._id, + }); + res.status(200).json({ message: QN_RETRIEVED_MESSAGE, - question: formatQuestionResponse(randomQuestion[0]), + question: formatQuestionIndivResponse( + chosenQuestion, + questionTemplate as IQuestionTemplate, + ), }); } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); From 87e07dd4cf82217a72d3fec7b1e80f4e15890eaf Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sun, 3 Nov 2024 21:42:54 +0800 Subject: [PATCH 074/192] Allow editor to be initialized with template --- .../src/handlers/websocketHandler.ts | 294 ++++-------------- frontend/src/components/CodeEditor/index.tsx | 15 +- frontend/src/pages/CollabSandbox/index.tsx | 16 +- frontend/src/utils/collabCursor.ts | 6 +- frontend/src/utils/collabSocket.ts | 52 ++-- 5 files changed, 119 insertions(+), 264 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index ef966ac2b9..58b8c30839 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -1,59 +1,52 @@ import { Socket } from "socket.io"; import { io } from "../server"; import redisClient from "../config/redis"; -// import { ChangeSet, Text } from "@codemirror/state"; -// import { rebaseUpdates, Update } from "@codemirror/collab"; import { Doc, Text, applyUpdate, encodeStateAsUpdate } from "yjs"; enum CollabEvents { // Receive JOIN = "join", - // CHANGE = "change", LEAVE = "leave", DISCONNECT = "disconnect", + INIT_DOCUMENT = "init_document", UPDATE_REQUEST = "update_request", UPDATE_CURSOR_REQUEST = "update_cursor_request", - PUSH_UPDATES = "push_updates", - PULL_UPDATES = "pull_updates", - INIT_DOCUMENT = "init_document", - GET_DOCUMENT = "get_document", - // Send ROOM_FULL = "room_full", USER_CONNECTED = "user_connected", NEW_USER_CONNECTED = "new_user_connected", - // CODE_CHANGE = "code_change", PARTNER_LEFT = "partner_left", PARTNER_DISCONNECTED = "partner_disconnected", SYNC = "sync", UPDATE = "update", UPDATE_CURSOR = "update_cursor", - - PULL_UPDATES_RESPONSE = "pull_updates_response", - GET_DOCUMENT_RESPONSE = "get_document_response", } -const EXPIRY_TIME = 3600; - // interface CollabSession { -// updates: Update[]; // updates.length = current version -// doc: Text; -// pendingPullUpdatesRequests: ((updates: Update[]) => void)[]; +// doc: Doc; +// areBothUsersConnected: boolean; // } -// const collabSessions = new Map(); +const EXPIRY_TIME = 3600; const collabSessions = new Map(); +const roomReadiness = new Map(); + +const CONNECTION_DELAY = 3000; // time window to allow for page re-renders / refresh +const userConnections = new Map(); export const handleWebsocketCollabEvents = (socket: Socket) => { - socket.on(CollabEvents.JOIN, async (roomId: string) => { - if (!roomId) { + socket.on(CollabEvents.JOIN, async (uid: string, roomId: string) => { + const connectionKey = `${uid}:${roomId}`; + if (userConnections.has(connectionKey)) { + clearTimeout(userConnections.get(connectionKey)!); return; } + userConnections.set(connectionKey, null); const room = io.sockets.adapter.rooms.get(roomId); - if (room && room.size >= 2) { + if (room && room?.size >= 2) { socket.emit(CollabEvents.ROOM_FULL); return; } @@ -61,28 +54,40 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { socket.join(roomId); socket.data.roomId = roomId; - // in case of disconnect, send the code to the user when he rejoins - // const collabSession = await redisClient.get(`collaboration:${roomId}`); - // if (collabSession) { - // if (!yCollabSessions.has(roomId)) { - // yCollabSessions.set(roomId, JSON.parse(collabSession) as Y.Doc); - // } - // } else { - // const ydoc = new Y.Doc(); - // yCollabSessions.set(roomId, ydoc); - // } - if (!collabSessions.has(roomId)) { + if ( + io.sockets.adapter.rooms.get(roomId)?.size === 2 && + !collabSessions.has(roomId) + ) { + console.log("create collab session: ", roomId); + const doc = new Doc(); + doc.on(CollabEvents.UPDATE, (update) => { + console.log("server doc updated: ", roomId); + io.to(roomId).emit(CollabEvents.UPDATE, update); + }); + collabSessions.set(roomId, doc); + roomReadiness.set(roomId, false); + + io.to(roomId).emit(CollabEvents.SYNC); + } + }); + + socket.on(CollabEvents.INIT_DOCUMENT, (roomId: string, template: string) => { + let doc = collabSessions.get(roomId); + if (!doc) { + doc = new Doc(); } - socket.emit( - CollabEvents.SYNC, - encodeStateAsUpdate(collabSessions.get(roomId)!) - ); - socket.emit(CollabEvents.USER_CONNECTED); - // inform the other user that a new user has joined - socket.to(roomId).emit(CollabEvents.NEW_USER_CONNECTED); + const isPartnerReady = roomReadiness.get(roomId); + console.log("partner ready: ", isPartnerReady); + console.log("doc: ", doc.getText().length); + if (isPartnerReady && doc.getText().length === 0) { + console.log("insert template"); + doc.getText().insert(0, template); + } else { + roomReadiness.set(roomId, true); + } }); socket.on( @@ -94,7 +99,8 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { } applyUpdate(doc, update); - socket.to(roomId).emit(CollabEvents.UPDATE, update); + // socket.to(roomId).emit(CollabEvents.UPDATE, update); + // socket.to(roomId).emit(CollabEvents.UPDATE, encodeStateAsUpdate(doc)); } ); @@ -108,198 +114,28 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { } ); - socket.on(CollabEvents.LEAVE, (roomId: string) => { - if (!roomId) { + socket.on(CollabEvents.LEAVE, (uid: string, roomId: string) => { + const connectionKey = `${uid}:${roomId}`; + if (!userConnections.has(connectionKey)) { return; } - socket.leave(roomId); - const room = io.sockets.adapter.rooms.get(roomId); - if (room?.size === 0) { - collabSessions.delete(roomId); - } else { - socket.to(roomId).emit(CollabEvents.PARTNER_LEFT); - } - }); -}; - -// export const handleWebsocketCollabEvents = (socket: Socket) => { -// socket.on(CollabEvents.JOIN, async (roomId: string) => { -// if (!roomId) { -// return; -// } + clearTimeout(userConnections.get(connectionKey)!); -// const room = io.sockets.adapter.rooms.get(roomId); -// if (room && room.size >= 2) { -// socket.emit(CollabEvents.ROOM_FULL); -// return; -// } + const connectionTimeout = setTimeout(() => { + userConnections.delete(connectionKey); + socket.leave(roomId); + socket.disconnect(); -// socket.join(roomId); -// socket.data.roomId = roomId; - -// // in case of disconnect, send the code to the user when he rejoins -// const collabSession = await redisClient.get(`collaboration:${roomId}`); -// if (collabSession) { -// if (!collabSessions.has(roomId)) { -// collabSessions.set(roomId, JSON.parse(collabSession) as CollabSession); -// } -// } else { -// initCollabSession(roomId); -// } -// socket.emit(CollabEvents.USER_CONNECTED); - -// // inform the other user that a new user has joined -// socket.to(roomId).emit(CollabEvents.NEW_USER_CONNECTED); -// }); - -// // socket.on(CollabEvents.CHANGE, async (roomId: string, code: string) => { -// // if (!roomId || !code) { -// // return; -// // } - -// // await redisClient.set(`collaboration:${roomId}`, code, { -// // EX: EXPIRY_TIME, -// // }); -// // socket.to(roomId).emit(CollabEvents.CODE_CHANGE, code); -// // }); - -// socket.on(CollabEvents.LEAVE, (roomId: string) => { -// if (!roomId) { -// return; -// } - -// socket.leave(roomId); -// const room = io.sockets.adapter.rooms.get(roomId); -// if (room?.size === 0) { -// collabSessions.delete(roomId); -// } else { -// socket.to(roomId).emit(CollabEvents.PARTNER_LEFT); -// } -// }); - -// socket.on(CollabEvents.DISCONNECT, () => { -// const { roomId } = socket.data; -// if (roomId) { -// socket.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED); -// } -// }); - -// handleCodeEditorEvents(socket); -// }; - -// /* Code Editor Events */ -// // Adapted from https://codemirror.net/examples/collab/ and https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets - -// const handleCodeEditorEvents = (socket: Socket) => { -// socket.on( -// CollabEvents.INIT_DOCUMENT, -// (roomId: string, template: string, callback: () => void) => { -// initCollabSession(roomId, template); -// callback(); -// } -// ); - -// socket.on(CollabEvents.GET_DOCUMENT, (roomId: string) => { -// const { updates, doc } = initCollabSession(roomId); -// socket.emit( -// CollabEvents.GET_DOCUMENT_RESPONSE, -// updates.length, -// doc.toString() -// ); -// }); - -// socket.on(CollabEvents.PULL_UPDATES, (roomId: string, version: number) => { -// const { updates, pendingPullUpdatesRequests } = initCollabSession(roomId); -// if (version < updates.length) { -// // send the new updates -// socket.emit( -// CollabEvents.PULL_UPDATES_RESPONSE, -// JSON.stringify(updates.slice(version)) -// ); -// } else { -// // wait until there are new updates to send -// pendingPullUpdatesRequests.push((updates) => { -// socket.emit( -// CollabEvents.PULL_UPDATES_RESPONSE, -// JSON.stringify(updates.slice(version)) -// ); -// }); -// } -// }); - -// // received new updates, notify any pending pullUpdates requests -// socket.on( -// CollabEvents.PUSH_UPDATES, -// async ( -// roomId: string, -// version: number, -// newUpdates: string, -// callback: () => void -// ) => { -// const { updates, doc, pendingPullUpdatesRequests } = -// initCollabSession(roomId); -// let docUpdates = JSON.parse(newUpdates) as readonly Update[]; - -// try { -// // If the given version is the latest version, apply the new updates. -// // Else, rebase updates first. -// if (version < updates.length) { -// docUpdates = rebaseUpdates(docUpdates, updates.slice(version)); -// } - -// for (const update of docUpdates) { -// const changes = ChangeSet.fromJSON(update.changes); -// updates.push({ -// clientID: update.clientID, -// changes: changes, -// effects: update.effects, -// }); - -// const updatedCollabSession = { -// updates: updates, -// doc: changes.apply(doc), -// pendingPullUpdatesRequests: pendingPullUpdatesRequests, -// }; -// collabSessions.set(roomId, updatedCollabSession); - -// await redisClient.set( -// `collaboration:${roomId}`, -// JSON.stringify(updatedCollabSession), -// { -// EX: EXPIRY_TIME, -// } -// ); -// } -// callback(); - -// while (pendingPullUpdatesRequests.length) { -// pendingPullUpdatesRequests.pop()!(updates); -// } -// } catch (error) { -// console.error(error); -// callback(); -// } -// } -// ); -// }; + const room = io.sockets.adapter.rooms.get(roomId); + if (!room || room.size === 0) { + console.log("delete collab session: ", roomId); + collabSessions.get(roomId)?.destroy(); + collabSessions.delete(roomId); + roomReadiness.delete(roomId); + } + }, CONNECTION_DELAY); -// const initCollabSession = ( -// roomId: string, -// template?: string -// ): CollabSession => { -// const collabSession = collabSessions.get(roomId); -// if (!collabSession) { -// collabSessions.set(roomId, { -// updates: [], -// doc: Text.of([template ? template : ""]), -// pendingPullUpdatesRequests: [], -// }); -// } else if (template) { -// collabSessions.set(roomId, { -// ...collabSession, -// doc: Text.of([template]), -// }); -// } -// return collabSessions.get(roomId)!; -// }; + userConnections.set(connectionKey, connectionTimeout); + }); +}; diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index df1e97838d..17228c6952 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -3,8 +3,8 @@ import { langs } from "@uiw/codemirror-extensions-langs"; import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; import { EditorView } from "@codemirror/view"; import { EditorState } from "@codemirror/state"; -import { useEffect } from "react"; -import { removeCursorListener } from "../../utils/collabSocket"; +import { useEffect, useRef } from "react"; +import { initDocument, removeCursorListener } from "../../utils/collabSocket"; import { cursorExtension } from "../../utils/collabCursor"; import { yCollab } from "y-codemirror.next"; import { Text } from "yjs"; @@ -37,8 +37,17 @@ const CodeEditor: React.FC = (props) => { isReadOnly = false, } = props; + const effectRan = useRef(false); + useEffect(() => { - return () => removeCursorListener(); + if (!effectRan.current) { + initDocument(roomId, "code template"); + } + + return () => { + effectRan.current = true; + removeCursorListener(); + }; }, []); return ( diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 690118c329..f2970c43d9 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -21,17 +21,11 @@ import reducer, { import QuestionDetailComponent from "../../components/QuestionDetail"; import { Navigate } from "react-router-dom"; import CodeEditor from "../../components/CodeEditor"; -import { join, leave } from "../../utils/collabSocket"; -import { Text } from "yjs"; -import { Awareness } from "y-protocols/awareness"; +import { CollabData, join, leave } from "../../utils/collabSocket"; const CollabSandbox: React.FC = () => { - // const [connected, setConnected] = useState(false); const [showErrorScreen, setShowErrorScreen] = useState(false); - const [editorState, setEditorState] = useState<{ - text: Text; - awareness: Awareness; - } | null>(null); + const [editorState, setEditorState] = useState(null); const match = useMatch(); if (!match) { @@ -65,13 +59,13 @@ const CollabSandbox: React.FC = () => { } getQuestionById(questionId, dispatch); - if (!matchId || editorState) { + if (!matchUser || !matchId) { return; } const connectToCollabSession = async () => { try { - const { text, awareness } = await join(matchId); + const { text, awareness } = await join(matchUser.id, matchId); setEditorState({ text, awareness }); } catch (error) { console.error("Error connecting to collab session: ", error); @@ -80,7 +74,7 @@ const CollabSandbox: React.FC = () => { connectToCollabSession(); - return () => leave(matchId); + return () => leave(matchUser.id, matchId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/frontend/src/utils/collabCursor.ts b/frontend/src/utils/collabCursor.ts index b2e435b473..2cfa18b1da 100644 --- a/frontend/src/utils/collabCursor.ts +++ b/frontend/src/utils/collabCursor.ts @@ -5,7 +5,7 @@ import { WidgetType, } from "@codemirror/view"; import { StateField, StateEffect } from "@codemirror/state"; -import { receiveCursorUpdates, sendCursorUpdates } from "./collabSocket"; +import { receiveCursorUpdate, sendCursorUpdate } from "./collabSocket"; // Adapted from https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets @@ -149,11 +149,11 @@ export const cursorExtension = ( to: transaction.selection.ranges[0].to, }; - sendCursorUpdates(roomId, cursor); + sendCursorUpdate(roomId, cursor); } }); - receiveCursorUpdates(update.view); + receiveCursorUpdate(update.view); }), ]; }; diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 1a74f82754..8ab1b18abf 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -1,68 +1,84 @@ import { EditorView } from "@codemirror/view"; -// import { Text } from "@codemirror/state"; import { io } from "socket.io-client"; import { updateCursor, Cursor } from "./collabCursor"; import { Doc, Text, applyUpdate } from "yjs"; import { Awareness } from "y-protocols/awareness"; -// Adapted from https://codemirror.net/examples/collab/ and https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets - enum CollabEvents { // Send JOIN = "join", LEAVE = "leave", + INIT_DOCUMENT = "init_document", UPDATE_REQUEST = "update_request", UPDATE_CURSOR_REQUEST = "update_cursor_request", // Receive - USER_CONNECTED = "user_connected", + // USER_CONNECTED = "user_connected", SYNC = "sync", UPDATE = "update", UPDATE_CURSOR = "update_cursor", } +export type CollabData = { + text: Text; + awareness: Awareness; +}; + const COLLAB_SOCKET_URL = "http://localhost:3003"; const collabSocket = io(COLLAB_SOCKET_URL, { reconnectionAttempts: 3, autoConnect: false, }); -export const join = ( - roomId: string -): Promise<{ text: Text; awareness: Awareness }> => { +let doc: Doc; +let text: Text; +let awareness: Awareness; + +export const join = (uid: string, roomId: string): Promise => { collabSocket.connect(); - collabSocket.emit(CollabEvents.JOIN, roomId); - const doc = new Doc(); - const text = doc.getText("codemirror"); - const awareness = new Awareness(doc); + doc = new Doc(); + text = doc.getText(); + awareness = new Awareness(doc); doc.on(CollabEvents.UPDATE, (update) => { + console.log("client sent update"); collabSocket.emit(CollabEvents.UPDATE_REQUEST, roomId, update); }); collabSocket.on(CollabEvents.UPDATE, (update) => { + console.log("client received update"); applyUpdate(doc, new Uint8Array(update)); }); + collabSocket.emit(CollabEvents.JOIN, uid, roomId); + console.log("join: ", roomId); + return new Promise((resolve) => { - collabSocket.once(CollabEvents.SYNC, (update) => { - applyUpdate(doc, new Uint8Array(update)); + // resolve({ text: text, awareness: awareness }); + collabSocket.once(CollabEvents.SYNC, () => { + console.log("sync"); resolve({ text: text, awareness: awareness }); }); }); }; -export const leave = (roomId: string) => { - collabSocket.emit(CollabEvents.LEAVE, roomId); - collabSocket.disconnect(); +export const initDocument = (roomId: string, template: string) => { + collabSocket.emit(CollabEvents.INIT_DOCUMENT, roomId, template); +}; + +export const leave = (uid: string, roomId: string) => { + console.log("leave: ", roomId); + collabSocket.off(CollabEvents.UPDATE); + collabSocket.emit(CollabEvents.LEAVE, uid, roomId); + doc.destroy(); }; -export const sendCursorUpdates = (roomId: string, cursor: Cursor) => { +export const sendCursorUpdate = (roomId: string, cursor: Cursor) => { collabSocket.emit(CollabEvents.UPDATE_CURSOR_REQUEST, roomId, cursor); }; -export const receiveCursorUpdates = (view: EditorView) => { +export const receiveCursorUpdate = (view: EditorView) => { if (collabSocket.hasListeners(CollabEvents.UPDATE_CURSOR)) { return; } From 8c279a384de61417ee9d80dff09379d430e00903 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 3 Nov 2024 22:27:24 +0800 Subject: [PATCH 075/192] Update code execution and fix some bugs --- backend/code-execution-service/.env.sample | 4 +- .../controllers/codeExecutionControllers.ts | 94 +++++++++++-------- .../src/utils/constants.ts | 2 +- .../src/utils/oneCompilerApi.ts | 2 +- .../src/utils/questionApi.ts | 12 +++ .../src/utils/testCasesApi.ts | 15 +++ backend/code-execution-service/swagger.yml | 20 +--- .../tests/codeExecutionRoutes.spec.ts | 56 +++++++++-- .../src/controllers/questionController.ts | 2 + .../question-service/src/models/Question.ts | 4 + frontend/src/components/Chat/index.tsx | 1 + frontend/src/components/TabPanel/index.tsx | 1 + frontend/src/pages/CollabSandbox/index.tsx | 6 +- 13 files changed, 151 insertions(+), 68 deletions(-) create mode 100644 backend/code-execution-service/src/utils/questionApi.ts create mode 100644 backend/code-execution-service/src/utils/testCasesApi.ts diff --git a/backend/code-execution-service/.env.sample b/backend/code-execution-service/.env.sample index 561d77ee13..66831ae5ae 100644 --- a/backend/code-execution-service/.env.sample +++ b/backend/code-execution-service/.env.sample @@ -5,4 +5,6 @@ ORIGINS=http://localhost:5173,http://127.0.0.1:5173 # One Compiler ONE_COMPILER_URL=https://onecompiler-apis.p.rapidapi.com/api/v1/run -ONE_COMPILER_KEY= \ No newline at end of file +ONE_COMPILER_KEY= + +QUESTION_SERVICE_URL=http://question-service:3000/api/questions \ No newline at end of file diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index f84aa361e2..d167673414 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -8,6 +8,8 @@ import { ERROR_NOT_SAME_LENGTH_MESSAGE, SUCCESS_MESSAGE, } from "../utils/constants"; +import { questionService } from "../utils/questionApi"; +import { testCasesApi } from "../utils/testCasesApi"; interface CompilerResult { status: string; @@ -19,14 +21,9 @@ interface CompilerResult { } export const executeCode = async (req: Request, res: Response) => { - const { - language, - code, - stdinList, - stdoutList: expectedStdoutList, - } = req.body; + const { questionId, language, code } = req.body; - if (!language || !code || !stdinList || !expectedStdoutList) { + if (!language || !code || !questionId) { res.status(400).json({ message: ERROR_MISSING_REQUIRED_FIELDS_MESSAGE, }); @@ -40,45 +37,62 @@ export const executeCode = async (req: Request, res: Response) => { return; } - if (stdinList.length !== expectedStdoutList.length) { - res.status(400).json({ - message: ERROR_NOT_SAME_LENGTH_MESSAGE, - }); - return; - } - try { - const response = await oneCompilerApi(language, stdinList, code); + // Get question test case files + const qnsResponse = await questionService.get(`/${questionId}`); + const { testcaseInputFileUrl, testcaseOutputFileUrl } = + qnsResponse.data.question; - const data = (response.data as CompilerResult[]).map((result, index) => { - const { - status, - exception, - stdout: actualStdout, - stderr, - stdin, - executionTime, - } = result; - const expectedStdout = expectedStdoutList[index]; + // Extract test cases from input and output files + const testCases = await testCasesApi( + testcaseInputFileUrl, + testcaseOutputFileUrl + ); - return { - status, - exception, - expectedStdout, - actualStdout, - stderr, - stdin, - executionTime, - isMatch: - stderr !== null - ? false - : actualStdout.trim() === expectedStdout.trim(), - }; - }); + const stdinList: string[] = testCases.input; + const expectedStdoutList: string[] = testCases.output; + + if (stdinList.length !== expectedStdoutList.length) { + res.status(400).json({ + message: ERROR_NOT_SAME_LENGTH_MESSAGE, + }); + return; + } + + // Execute code for each test case + const compilerResponse = await oneCompilerApi(language, stdinList, code); + + const compilerData = (compilerResponse.data as CompilerResult[]).map( + (result, index) => { + const { + status, + exception, + stdout: actualStdout, + stderr, + stdin, + executionTime, + } = result; + const expectedStdout = expectedStdoutList[index]; + + return { + status, + exception, + expectedStdout, + actualStdout, + stderr, + stdin, + executionTime, + isMatch: + stderr !== null + ? false + : actualStdout.trim() === expectedStdout.trim(), + }; + } + ); res.status(200).json({ message: SUCCESS_MESSAGE, - data, + data: compilerData, }); } catch (err) { console.log(err); diff --git a/backend/code-execution-service/src/utils/constants.ts b/backend/code-execution-service/src/utils/constants.ts index 1e5fb3aed2..dedf32abef 100644 --- a/backend/code-execution-service/src/utils/constants.ts +++ b/backend/code-execution-service/src/utils/constants.ts @@ -1,7 +1,7 @@ export const SUPPORTED_LANGUAGES = ["python", "java", "c"]; export const ERROR_MISSING_REQUIRED_FIELDS_MESSAGE = - "Missing required fields: language, code, stdinList, or stdoutList"; + "Missing required fields: language, code, or questionId."; export const ERROR_UNSUPPORTED_LANGUAGE_MESSAGE = "Unsupported language."; diff --git a/backend/code-execution-service/src/utils/oneCompilerApi.ts b/backend/code-execution-service/src/utils/oneCompilerApi.ts index 143ecacb2f..b5936510a0 100644 --- a/backend/code-execution-service/src/utils/oneCompilerApi.ts +++ b/backend/code-execution-service/src/utils/oneCompilerApi.ts @@ -10,7 +10,7 @@ interface FileType { export const oneCompilerApi = async ( language: string, - stdin: string, + stdin: string[], userCode: string ) => { let files: FileType[] = []; diff --git a/backend/code-execution-service/src/utils/questionApi.ts b/backend/code-execution-service/src/utils/questionApi.ts new file mode 100644 index 0000000000..76c414befc --- /dev/null +++ b/backend/code-execution-service/src/utils/questionApi.ts @@ -0,0 +1,12 @@ +import axios from "axios"; + +const QUESTION_SERVICE_URL = + process.env.QUESTION_SERVICE_URL || + "http://question-service:3000/api/questions"; + +export const questionService = axios.create({ + baseURL: QUESTION_SERVICE_URL, + headers: { + "Content-Type": "application/json", + }, +}); diff --git a/backend/code-execution-service/src/utils/testCasesApi.ts b/backend/code-execution-service/src/utils/testCasesApi.ts new file mode 100644 index 0000000000..64936fbd17 --- /dev/null +++ b/backend/code-execution-service/src/utils/testCasesApi.ts @@ -0,0 +1,15 @@ +import axios from "axios"; + +export const testCasesApi = async ( + inputFileUrl: string, + outputFileUrl: string +) => { + const inputFileUrlResponse = await axios.get(inputFileUrl); + const outputFileUrlResponse = await axios.get(outputFileUrl); + + // Split the input and output files by double new line + return { + input: inputFileUrlResponse.data.split(/\r?\n\r?\n/), + output: outputFileUrlResponse.data.split(/\r?\n\r?\n/), + }; +}; diff --git a/backend/code-execution-service/swagger.yml b/backend/code-execution-service/swagger.yml index a2518f4ead..0beaf2978e 100644 --- a/backend/code-execution-service/swagger.yml +++ b/backend/code-execution-service/swagger.yml @@ -83,22 +83,10 @@ paths: type: string description: The source code to execute. example: "name = input()\nage = input()\nprint('Hello ' + name + '. You are ' + age + ' years old this year?')\n\n" - stdinList: - type: array - description: List of standard input values to pass to the code. - items: - type: string - example: ["Alice\n21", "Peter\n22"] - stdoutList: - type: array - description: Expected standard output values to compare against. - items: - type: string - example: - [ - "Hello Alice. You are 21 years old this year?\n", - "Hello Peter. You are 22 years old this year?\n", - ] + questionId: + type: string + description: Question ID. + example: "123456789" responses: 200: description: Execution Result diff --git a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts index 202c712927..55764f4614 100644 --- a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts +++ b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts @@ -6,12 +6,28 @@ import { ERROR_NOT_SAME_LENGTH_MESSAGE, SUCCESS_MESSAGE, } from "../src/utils/constants"; +import { testCasesApi } from "../src/utils/testCasesApi"; +import { questionService } from "../src/utils/questionApi"; const request = supertest(app); const BASE_URL = "/api"; +jest.mock("../src/utils/questionApi", () => ({ + questionService: { + get: jest.fn(), + }, +})); + +jest.mock("../src/utils/testCasesApi", () => ({ + testCasesApi: jest.fn(), +})); + describe("Code execution routes", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe("GET /", () => { it("should return 200 OK", (done) => { request.get("/").expect(200, done); @@ -31,35 +47,63 @@ describe("Code execution routes", () => { const response = await request.post(`${BASE_URL}/run`).send({ language: "testing1234", code: "print('Hello, world!')", - stdinList: ["input"], - stdoutList: ["Hello, world!"], + questionId: "1234", }); expect(response.status).toBe(400); expect(response.body.message).toBe(ERROR_UNSUPPORTED_LANGUAGE_MESSAGE); }); it("should return 400 if stdinList and stdoutList lengths do not match", async () => { + (questionService.get as jest.Mock).mockResolvedValue({ + data: { + question: { + testcaseInputFileUrl: "https://peerprep.com/input", + testcaseOutputFileUrl: "https://peerprep.com/output", + }, + }, + }); + + (testCasesApi as jest.Mock).mockResolvedValue({ + input: ["1", "2"], + output: ["1"], + }); + const response = await request.post(`${BASE_URL}/run`).send({ language: "python", code: "print('Hello, world!')", - stdinList: ["input1"], - stdoutList: ["output1", "output2"], + questionId: "1234", }); expect(response.status).toBe(400); expect(response.body.message).toBe(ERROR_NOT_SAME_LENGTH_MESSAGE); }); it("should return 200 and execution result when code executes successfully", async () => { + (questionService.get as jest.Mock).mockResolvedValue({ + data: { + question: { + testcaseInputFileUrl: "https://peerprep.com/input", + testcaseOutputFileUrl: "https://peerprep.com/output", + }, + }, + }); + + (testCasesApi as jest.Mock).mockResolvedValue({ + input: ["1", "2"], + output: ["1", "4"], + }); + const response = await request.post(`${BASE_URL}/run`).send({ language: "python", code: "print(input())", - stdinList: ["Hello, world!"], - stdoutList: ["Hello, world!"], + questionId: "1234", }); + expect(response.status).toBe(200); expect(response.body.message).toBe(SUCCESS_MESSAGE); expect(response.body.data).toBeInstanceOf(Array); expect(response.body.data[0]).toHaveProperty("isMatch", true); + expect(response.body.data[0]["isMatch"]).toBe(true); + expect(response.body.data[1]["isMatch"]).toBe(false); }); }); }); diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index ccce137ba6..501c47737f 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -356,6 +356,8 @@ const formatQuestionIndivResponse = ( description: question.description, complexity: question.complexity, categories: question.category, + testcaseInputFileUrl: question.testcaseInputFileUrl, + testcaseOutputFileUrl: question.testcaseOutputFileUrl, pythonTemplate: questionTemplate ? questionTemplate.pythonTemplate : "", javaTemplate: questionTemplate ? questionTemplate.javaTemplate : "", cTemplate: questionTemplate ? questionTemplate.cTemplate : "", diff --git a/backend/question-service/src/models/Question.ts b/backend/question-service/src/models/Question.ts index 40e86f6fb3..c8a8b9cbfa 100644 --- a/backend/question-service/src/models/Question.ts +++ b/backend/question-service/src/models/Question.ts @@ -5,6 +5,8 @@ export interface IQuestion extends Document { description: string; complexity: string; category: string[]; + testcaseInputFileUrl: string; + testcaseOutputFileUrl: string; createdAt: Date; updatedAt: Date; } @@ -22,6 +24,8 @@ const questionSchema: Schema = new mongoose.Schema( type: [String], required: true, }, + testcaseInputFileUrl: { type: String, required: true }, + testcaseOutputFileUrl: { type: String, required: true }, }, { timestamps: true }, ); diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 8c5da246c3..84df8e15f7 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -39,6 +39,7 @@ const StyledTypography = styled(Typography)(({ theme }) => ({ borderRadius: theme.spacing(2), maxWidth: "80%", whiteSpace: "pre-line", + wordBreak: "break-word", })); const Chat: React.FC = ({ isActive }) => { diff --git a/frontend/src/components/TabPanel/index.tsx b/frontend/src/components/TabPanel/index.tsx index 4dbea7c95e..58a19a9fdb 100644 --- a/frontend/src/components/TabPanel/index.tsx +++ b/frontend/src/components/TabPanel/index.tsx @@ -12,6 +12,7 @@ const TabPanel: React.FC = ({ children, value, selected }) => { role="tabpanel" sx={(theme) => ({ display: selected === value ? "flex" : "none", + overflow: "auto", flexDirection: "column", padding: theme.spacing(0, 2), flex: 1, diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 9d2bf9afb4..26e52c1f46 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -198,7 +198,6 @@ const CollabSandbox: React.FC = () => { sx={{ flex: 1, maxHeight: "50vh", - overflow: "auto", display: "flex", flexDirection: "column", }} @@ -217,7 +216,8 @@ const CollabSandbox: React.FC = () => { - + + ({ margin: theme.spacing(2, 0) })}> {[...Array(testcases.length)] .map((_, index) => index + 1) @@ -242,7 +242,7 @@ const CollabSandbox: React.FC = () => { result={testcases[selectedTestcase].result} /> - + From 3edb8fe3e1163cbcb190f20b47c79c3dd4b14e14 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sun, 3 Nov 2024 23:38:21 +0800 Subject: [PATCH 076/192] Upgrade to yjs v2 --- .../src/handlers/websocketHandler.ts | 116 ++++++++++-------- frontend/src/pages/CollabSandbox/index.tsx | 18 ++- frontend/src/utils/collabSocket.ts | 32 ++--- 3 files changed, 95 insertions(+), 71 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 58b8c30839..4a47d90ffa 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -1,7 +1,7 @@ import { Socket } from "socket.io"; import { io } from "../server"; import redisClient from "../config/redis"; -import { Doc, Text, applyUpdate, encodeStateAsUpdate } from "yjs"; +import { Doc, applyUpdateV2, encodeStateAsUpdateV2 } from "yjs"; enum CollabEvents { // Receive @@ -13,28 +13,17 @@ enum CollabEvents { UPDATE_CURSOR_REQUEST = "update_cursor_request", // Send - ROOM_FULL = "room_full", - USER_CONNECTED = "user_connected", - NEW_USER_CONNECTED = "new_user_connected", - PARTNER_LEFT = "partner_left", - PARTNER_DISCONNECTED = "partner_disconnected", - SYNC = "sync", - UPDATE = "update", + ROOM_READY = "room_ready", + UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", } -// interface CollabSession { -// doc: Doc; -// areBothUsersConnected: boolean; -// } - const EXPIRY_TIME = 3600; - -const collabSessions = new Map(); -const roomReadiness = new Map(); - const CONNECTION_DELAY = 3000; // time window to allow for page re-renders / refresh + const userConnections = new Map(); +const collabSessions = new Map(); +const partnerReadiness = new Map(); export const handleWebsocketCollabEvents = (socket: Socket) => { socket.on(CollabEvents.JOIN, async (uid: string, roomId: string) => { @@ -47,7 +36,7 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { const room = io.sockets.adapter.rooms.get(roomId); if (room && room?.size >= 2) { - socket.emit(CollabEvents.ROOM_FULL); + socket.emit(CollabEvents.ROOM_READY, false); return; } @@ -58,49 +47,27 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { io.sockets.adapter.rooms.get(roomId)?.size === 2 && !collabSessions.has(roomId) ) { - console.log("create collab session: ", roomId); - - const doc = new Doc(); - doc.on(CollabEvents.UPDATE, (update) => { - console.log("server doc updated: ", roomId); - io.to(roomId).emit(CollabEvents.UPDATE, update); - }); - - collabSessions.set(roomId, doc); - roomReadiness.set(roomId, false); - - io.to(roomId).emit(CollabEvents.SYNC); + createCollabSession(roomId); + io.to(roomId).emit(CollabEvents.ROOM_READY, true); } }); socket.on(CollabEvents.INIT_DOCUMENT, (roomId: string, template: string) => { - let doc = collabSessions.get(roomId); - if (!doc) { - doc = new Doc(); - } + const doc = getDocument(roomId); + const isPartnerReady = partnerReadiness.get(roomId); - const isPartnerReady = roomReadiness.get(roomId); - console.log("partner ready: ", isPartnerReady); - console.log("doc: ", doc.getText().length); if (isPartnerReady && doc.getText().length === 0) { - console.log("insert template"); doc.getText().insert(0, template); } else { - roomReadiness.set(roomId, true); + partnerReadiness.set(roomId, true); } }); socket.on( CollabEvents.UPDATE_REQUEST, (roomId: string, update: Uint8Array) => { - let doc = collabSessions.get(roomId); - if (!doc) { - doc = new Doc(); - } - applyUpdate(doc, update); - - // socket.to(roomId).emit(CollabEvents.UPDATE, update); - // socket.to(roomId).emit(CollabEvents.UPDATE, encodeStateAsUpdate(doc)); + const doc = getDocument(roomId); + applyUpdateV2(doc, new Uint8Array(update)); } ); @@ -129,13 +96,60 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { const room = io.sockets.adapter.rooms.get(roomId); if (!room || room.size === 0) { - console.log("delete collab session: ", roomId); - collabSessions.get(roomId)?.destroy(); - collabSessions.delete(roomId); - roomReadiness.delete(roomId); + removeCollabSession(roomId); } }, CONNECTION_DELAY); userConnections.set(connectionKey, connectionTimeout); }); }; + +const createCollabSession = (roomId: string) => { + console.log("set up collab session: ", roomId); + const doc = new Doc(); + doc.on(CollabEvents.UPDATE, (update) => { + console.log("server doc updated: ", roomId); + // await saveDocument(roomId, doc); + io.to(roomId).emit(CollabEvents.UPDATE, update); + }); + + collabSessions.set(roomId, doc); + partnerReadiness.set(roomId, false); +}; + +const removeCollabSession = (roomId: string) => { + console.log("delete collab session: ", roomId); + collabSessions.get(roomId)?.destroy(); + collabSessions.delete(roomId); + partnerReadiness.delete(roomId); +}; + +// const saveDocument = async (roomId: string, doc: Doc) => { +// const decodedDoc = new TextDecoder().decode(encodeStateAsUpdateV2(doc)); +// await redisClient.set(`collaboration:${roomId}`, decodedDoc, { +// EX: EXPIRY_TIME, +// }); +// }; + +const getDocument = (roomId: string) => { + let doc = collabSessions.get(roomId); + if (!doc) { + console.log("no document in collabSessions"); + doc = new Doc(); + // const redisData = await redisClient.get(`collaboration:${roomId}`); + // if (redisData) { + // console.log("use redis document"); + // const update = new TextEncoder().encode(redisData); + // applyUpdateV2(doc, new Uint8Array(update)); + // } + + // doc.on(CollabEvents.UPDATE, async (update) => { + // console.log("server doc updated: ", roomId); + // await saveDocument(roomId, doc!); + // io.to(roomId).emit(CollabEvents.UPDATE, update); + // }); + // collabSessions.set(roomId, doc); + } + + return doc; +}; diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index f2970c43d9..b1bd57a2f3 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -21,11 +21,13 @@ import reducer, { import QuestionDetailComponent from "../../components/QuestionDetail"; import { Navigate } from "react-router-dom"; import CodeEditor from "../../components/CodeEditor"; -import { CollabData, join, leave } from "../../utils/collabSocket"; +import { CollabSessionData, join, leave } from "../../utils/collabSocket"; const CollabSandbox: React.FC = () => { const [showErrorScreen, setShowErrorScreen] = useState(false); - const [editorState, setEditorState] = useState(null); + const [editorState, setEditorState] = useState( + null + ); const match = useMatch(); if (!match) { @@ -65,8 +67,12 @@ const CollabSandbox: React.FC = () => { const connectToCollabSession = async () => { try { - const { text, awareness } = await join(matchUser.id, matchId); - setEditorState({ text, awareness }); + const editorState = await join(matchUser.id, matchId); + if (editorState.ready) { + setEditorState(editorState); + } else { + setShowErrorScreen(true); + } } catch (error) { console.error("Error connecting to collab session: ", error); } @@ -104,8 +110,8 @@ const CollabSandbox: React.FC = () => { if (showErrorScreen) { return ( ); } diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 8ab1b18abf..8a21a32a93 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -1,7 +1,7 @@ import { EditorView } from "@codemirror/view"; import { io } from "socket.io-client"; import { updateCursor, Cursor } from "./collabCursor"; -import { Doc, Text, applyUpdate } from "yjs"; +import { Doc, Text, applyUpdateV2 } from "yjs"; import { Awareness } from "y-protocols/awareness"; enum CollabEvents { @@ -13,13 +13,13 @@ enum CollabEvents { UPDATE_CURSOR_REQUEST = "update_cursor_request", // Receive - // USER_CONNECTED = "user_connected", - SYNC = "sync", - UPDATE = "update", + ROOM_READY = "room_ready", + UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", } -export type CollabData = { +export type CollabSessionData = { + ready: boolean; text: Text; awareness: Awareness; }; @@ -34,31 +34,35 @@ let doc: Doc; let text: Text; let awareness: Awareness; -export const join = (uid: string, roomId: string): Promise => { +export const join = ( + uid: string, + roomId: string +): Promise => { collabSocket.connect(); doc = new Doc(); text = doc.getText(); awareness = new Awareness(doc); - doc.on(CollabEvents.UPDATE, (update) => { - console.log("client sent update"); - collabSocket.emit(CollabEvents.UPDATE_REQUEST, roomId, update); + doc.on(CollabEvents.UPDATE, (update, origin) => { + if (origin != uid) { + console.log("client sent update"); + collabSocket.emit(CollabEvents.UPDATE_REQUEST, roomId, update); + } }); collabSocket.on(CollabEvents.UPDATE, (update) => { console.log("client received update"); - applyUpdate(doc, new Uint8Array(update)); + applyUpdateV2(doc, new Uint8Array(update), uid); }); collabSocket.emit(CollabEvents.JOIN, uid, roomId); console.log("join: ", roomId); return new Promise((resolve) => { - // resolve({ text: text, awareness: awareness }); - collabSocket.once(CollabEvents.SYNC, () => { - console.log("sync"); - resolve({ text: text, awareness: awareness }); + collabSocket.once(CollabEvents.ROOM_READY, (ready: boolean) => { + console.log("room ready: ", ready); + resolve({ ready: ready, text: text, awareness: awareness }); }); }); }; From e94ec373057aa8c9a7d55afb6ca179eaf0d52754 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 4 Nov 2024 01:23:24 +0800 Subject: [PATCH 077/192] Save code to redis --- .../src/handlers/websocketHandler.ts | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 4a47d90ffa..14f4495993 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -66,8 +66,12 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { socket.on( CollabEvents.UPDATE_REQUEST, (roomId: string, update: Uint8Array) => { - const doc = getDocument(roomId); - applyUpdateV2(doc, new Uint8Array(update)); + const doc = collabSessions.get(roomId); + if (doc) { + applyUpdateV2(doc, new Uint8Array(update)); + } else { + // TODO: error handling + } } ); @@ -106,14 +110,7 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { const createCollabSession = (roomId: string) => { console.log("set up collab session: ", roomId); - const doc = new Doc(); - doc.on(CollabEvents.UPDATE, (update) => { - console.log("server doc updated: ", roomId); - // await saveDocument(roomId, doc); - io.to(roomId).emit(CollabEvents.UPDATE, update); - }); - - collabSessions.set(roomId, doc); + getDocument(roomId); partnerReadiness.set(roomId, false); }; @@ -124,32 +121,35 @@ const removeCollabSession = (roomId: string) => { partnerReadiness.delete(roomId); }; -// const saveDocument = async (roomId: string, doc: Doc) => { -// const decodedDoc = new TextDecoder().decode(encodeStateAsUpdateV2(doc)); -// await redisClient.set(`collaboration:${roomId}`, decodedDoc, { -// EX: EXPIRY_TIME, -// }); -// }; - const getDocument = (roomId: string) => { let doc = collabSessions.get(roomId); if (!doc) { - console.log("no document in collabSessions"); doc = new Doc(); - // const redisData = await redisClient.get(`collaboration:${roomId}`); - // if (redisData) { - // console.log("use redis document"); - // const update = new TextEncoder().encode(redisData); - // applyUpdateV2(doc, new Uint8Array(update)); - // } - - // doc.on(CollabEvents.UPDATE, async (update) => { - // console.log("server doc updated: ", roomId); - // await saveDocument(roomId, doc!); - // io.to(roomId).emit(CollabEvents.UPDATE, update); - // }); - // collabSessions.set(roomId, doc); + doc.on(CollabEvents.UPDATE, async (update) => { + console.log("server doc updated: ", roomId); + saveDocument(roomId, doc!); + io.to(roomId).emit(CollabEvents.UPDATE, update); + }); + collabSessions.set(roomId, doc); } return doc; }; + +const saveDocument = async (roomId: string, doc: Doc) => { + const docState = encodeStateAsUpdateV2(doc); + const docAsString = Buffer.from(docState).toString("base64"); + await redisClient.set(`collaboration:${roomId}`, docAsString, { + EX: EXPIRY_TIME, + }); +}; + +// const getDocumentFromStore = async (roomId: string) => { +// const doc = getDocument(roomId); +// const storeData = await redisClient.get(`collaboration:${roomId}`); +// if (storeData) { +// const update = Buffer.from(storeData, "base64"); +// applyUpdateV2(doc, new Uint8Array(update)); +// } +// return !!storeData; +// }; From 2268430c385f81372b4cb6797bfc078c38f47284 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 4 Nov 2024 04:19:28 +0800 Subject: [PATCH 078/192] Partial handling of reconnection --- .../src/handlers/websocketHandler.ts | 36 ++++++++++----- frontend/src/components/CodeEditor/index.tsx | 3 +- frontend/src/utils/collabSocket.ts | 46 +++++++++++++++---- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 14f4495993..89298a80b7 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -11,6 +11,7 @@ enum CollabEvents { INIT_DOCUMENT = "init_document", UPDATE_REQUEST = "update_request", UPDATE_CURSOR_REQUEST = "update_cursor_request", + RECONNECT_REQUEST = "reconnect_request", // Send ROOM_READY = "room_ready", @@ -106,6 +107,27 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { userConnections.set(connectionKey, connectionTimeout); }); + + socket.on(CollabEvents.RECONNECT_REQUEST, async (roomId: string) => { + // TODO: Handle recconnection + socket.join(roomId); + + const doc = getDocument(roomId); + const storeData = await redisClient.get(`collaboration:${roomId}`); + + if (storeData) { + const tempDoc = new Doc(); + const update = Buffer.from(storeData, "base64"); + applyUpdateV2(tempDoc, new Uint8Array(update)); + const tempText = tempDoc.getText().toString(); + + const text = doc.getText(); + doc.transact(() => { + text.delete(0, text.length); + text.insert(0, tempText); + }); + } + }); }; const createCollabSession = (roomId: string) => { @@ -125,10 +147,10 @@ const getDocument = (roomId: string) => { let doc = collabSessions.get(roomId); if (!doc) { doc = new Doc(); - doc.on(CollabEvents.UPDATE, async (update) => { + doc.on(CollabEvents.UPDATE, (_update) => { console.log("server doc updated: ", roomId); saveDocument(roomId, doc!); - io.to(roomId).emit(CollabEvents.UPDATE, update); + io.to(roomId).emit(CollabEvents.UPDATE, encodeStateAsUpdateV2(doc!)); }); collabSessions.set(roomId, doc); } @@ -143,13 +165,3 @@ const saveDocument = async (roomId: string, doc: Doc) => { EX: EXPIRY_TIME, }); }; - -// const getDocumentFromStore = async (roomId: string) => { -// const doc = getDocument(roomId); -// const storeData = await redisClient.get(`collaboration:${roomId}`); -// if (storeData) { -// const update = Buffer.from(storeData, "base64"); -// applyUpdateV2(doc, new Uint8Array(update)); -// } -// return !!storeData; -// }; diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 17228c6952..166d12c90b 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -4,7 +4,7 @@ import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; import { EditorView } from "@codemirror/view"; import { EditorState } from "@codemirror/state"; import { useEffect, useRef } from "react"; -import { initDocument, removeCursorListener } from "../../utils/collabSocket"; +import { initDocument } from "../../utils/collabSocket"; import { cursorExtension } from "../../utils/collabCursor"; import { yCollab } from "y-codemirror.next"; import { Text } from "yjs"; @@ -46,7 +46,6 @@ const CodeEditor: React.FC = (props) => { return () => { effectRan.current = true; - removeCursorListener(); }; }, []); diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 8a21a32a93..8785efb7c3 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -11,11 +11,17 @@ enum CollabEvents { INIT_DOCUMENT = "init_document", UPDATE_REQUEST = "update_request", UPDATE_CURSOR_REQUEST = "update_cursor_request", + RECONNECT_REQUEST = "reconnect_request", // Receive ROOM_READY = "room_ready", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", + SOCKET_DISCONNECT = "disconnect", + SOCKET_CLIENT_DISCONNECT = "io client disconnect", + SOCKET_SERVER_DISCONNECT = "io server disconnect", + SOCKET_RECONNECT_SUCCESS = "reconnect", + SOCKET_RECONNECT_FAILED = "reconnect_failed", } export type CollabSessionData = { @@ -39,6 +45,7 @@ export const join = ( roomId: string ): Promise => { collabSocket.connect(); + initConnectionStatusListeners(roomId); doc = new Doc(); text = doc.getText(); @@ -46,22 +53,18 @@ export const join = ( doc.on(CollabEvents.UPDATE, (update, origin) => { if (origin != uid) { - console.log("client sent update"); collabSocket.emit(CollabEvents.UPDATE_REQUEST, roomId, update); } }); collabSocket.on(CollabEvents.UPDATE, (update) => { - console.log("client received update"); applyUpdateV2(doc, new Uint8Array(update), uid); }); collabSocket.emit(CollabEvents.JOIN, uid, roomId); - console.log("join: ", roomId); return new Promise((resolve) => { collabSocket.once(CollabEvents.ROOM_READY, (ready: boolean) => { - console.log("room ready: ", ready); resolve({ ready: ready, text: text, awareness: awareness }); }); }); @@ -72,8 +75,9 @@ export const initDocument = (roomId: string, template: string) => { }; export const leave = (uid: string, roomId: string) => { - console.log("leave: ", roomId); - collabSocket.off(CollabEvents.UPDATE); + collabSocket.removeAllListeners(); + collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_SUCCESS); + collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_FAILED); collabSocket.emit(CollabEvents.LEAVE, uid, roomId); doc.destroy(); }; @@ -94,6 +98,32 @@ export const receiveCursorUpdate = (view: EditorView) => { }); }; -export const removeCursorListener = () => { - collabSocket.off(CollabEvents.UPDATE_CURSOR); +export const reconnectRequest = (roomId: string) => { + collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); +}; + +const initConnectionStatusListeners = (roomId: string) => { + if (!collabSocket.hasListeners(CollabEvents.SOCKET_DISCONNECT)) { + collabSocket.on(CollabEvents.SOCKET_DISCONNECT, (reason) => { + if ( + reason !== CollabEvents.SOCKET_CLIENT_DISCONNECT && + reason !== CollabEvents.SOCKET_SERVER_DISCONNECT + ) { + // TODO: Handle socket disconnection + } + }); + } + + if (!collabSocket.io.hasListeners(CollabEvents.SOCKET_RECONNECT_SUCCESS)) { + collabSocket.io.on(CollabEvents.SOCKET_RECONNECT_SUCCESS, () => { + console.log("reconnect request"); + collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); + }); + } + + if (!collabSocket.io.hasListeners(CollabEvents.SOCKET_RECONNECT_FAILED)) { + collabSocket.io.on(CollabEvents.SOCKET_RECONNECT_FAILED, () => { + console.log("reconnect failed"); + }); + } }; From 65101182ff009c8c23cc9d8500f961e099ad259e Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 4 Nov 2024 10:21:53 +0800 Subject: [PATCH 079/192] Clean up --- backend/collab-service/src/handlers/websocketHandler.ts | 5 ++--- frontend/src/components/CodeEditor/index.tsx | 6 +++++- frontend/src/pages/Matched/index.tsx | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 89298a80b7..062cd1526d 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -17,6 +17,8 @@ enum CollabEvents { ROOM_READY = "room_ready", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", + // PARTNER_LEFT = "partner_left", + // PARTNER_DISCONNECTED = "partner_disconnected", } const EXPIRY_TIME = 3600; @@ -131,13 +133,11 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { }; const createCollabSession = (roomId: string) => { - console.log("set up collab session: ", roomId); getDocument(roomId); partnerReadiness.set(roomId, false); }; const removeCollabSession = (roomId: string) => { - console.log("delete collab session: ", roomId); collabSessions.get(roomId)?.destroy(); collabSessions.delete(roomId); partnerReadiness.delete(roomId); @@ -148,7 +148,6 @@ const getDocument = (roomId: string) => { if (!doc) { doc = new Doc(); doc.on(CollabEvents.UPDATE, (_update) => { - console.log("server doc updated: ", roomId); saveDocument(roomId, doc!); io.to(roomId).emit(CollabEvents.UPDATE, encodeStateAsUpdateV2(doc!)); }); diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 166d12c90b..8bab6cb8a5 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -40,8 +40,12 @@ const CodeEditor: React.FC = (props) => { const effectRan = useRef(false); useEffect(() => { + if (isReadOnly) { + return; + } + if (!effectRan.current) { - initDocument(roomId, "code template"); + initDocument(roomId, template); } return () => { diff --git a/frontend/src/pages/Matched/index.tsx b/frontend/src/pages/Matched/index.tsx index c545ac2f84..d801014df1 100644 --- a/frontend/src/pages/Matched/index.tsx +++ b/frontend/src/pages/Matched/index.tsx @@ -20,7 +20,7 @@ import { Navigate } from "react-router-dom"; import Loader from "../../components/Loader"; import { CheckCircleOutlineRounded } from "@mui/icons-material"; -const acceptanceTimeout = 10; +const acceptanceTimeout = 15; const Matched: React.FC = () => { const match = useMatch(); From 369c75a850e3283fecf6dac1bfb72e75b6a47ff6 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Mon, 4 Nov 2024 10:48:03 +0800 Subject: [PATCH 080/192] Remove testcase manual add --- .../src/controllers/questionController.ts | 3 --- backend/question-service/src/models/Question.ts | 14 -------------- .../QuestionTestCasesFileUpload/index.tsx | 2 +- frontend/src/pages/NewQuestion/index.tsx | 17 ----------------- frontend/src/pages/QuestionEdit/index.tsx | 17 ----------------- frontend/src/reducers/questionReducer.ts | 9 --------- frontend/src/utils/validators.ts | 15 --------------- 7 files changed, 1 insertion(+), 76 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 7dca447576..56eb302d90 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -41,7 +41,6 @@ export const createQuestion = async ( description, complexity, category, - testcases, testcaseInputFileUrl, testcaseOutputFileUrl, pythonTemplate, @@ -69,7 +68,6 @@ export const createQuestion = async ( description, complexity, category, - testcases, testcaseInputFileUrl, testcaseOutputFileUrl, }); @@ -445,7 +443,6 @@ const formatQuestionIndivResponse = ( description: question.description, complexity: question.complexity, categories: question.category, - testcases: question.testcases, testcaseInputFileUrl: question.testcaseInputFileUrl, testcaseOutputFileUrl: question.testcaseOutputFileUrl, pythonTemplate: questionTemplate ? questionTemplate.pythonTemplate : "", diff --git a/backend/question-service/src/models/Question.ts b/backend/question-service/src/models/Question.ts index 7dcebb77a3..c2dd19d491 100644 --- a/backend/question-service/src/models/Question.ts +++ b/backend/question-service/src/models/Question.ts @@ -1,29 +1,16 @@ import mongoose, { Schema, Document } from "mongoose"; -export interface ITestcase { - id: string; - input: string; - expectedOutput: string; -} - export interface IQuestion extends Document { title: string; description: string; complexity: string; category: string[]; - testcases: ITestcase[]; testcaseInputFileUrl: string; testcaseOutputFileUrl: string; createdAt: Date; updatedAt: Date; } -const testcaseSchema: Schema = new mongoose.Schema({ - id: { type: String, required: true }, - input: { type: String, required: true }, - expectedOutput: { type: String, required: true }, -}); - const questionSchema: Schema = new mongoose.Schema( { title: { type: String, required: true }, @@ -34,7 +21,6 @@ const questionSchema: Schema = new mongoose.Schema( required: true, }, category: { type: [String], required: true }, - testcases: { type: [testcaseSchema], required: true }, testcaseInputFileUrl: { type: String, required: true }, testcaseOutputFileUrl: { type: String, required: true }, }, diff --git a/frontend/src/components/QuestionTestCasesFileUpload/index.tsx b/frontend/src/components/QuestionTestCasesFileUpload/index.tsx index bb5ee0e539..f2963ea9e1 100644 --- a/frontend/src/components/QuestionTestCasesFileUpload/index.tsx +++ b/frontend/src/components/QuestionTestCasesFileUpload/index.tsx @@ -20,7 +20,7 @@ const QuestionTestCasesFileUpload: React.FC< }) => { return ( - + Test Cases File Upload { const [uploadedImagesUrl, setUploadedImagesUrl] = useState([]); const [isPreviewQuestion, setIsPreviewQuestion] = useState(false); - const [testCases, setTestCases] = useState([ - { id: uuidv4(), input: "", expectedOutput: "" }, - ]); const [testcaseInputFile, setTestcaseInputFile] = useState(null); const [testcaseOutputFile, setTestcaseOutputFile] = useState( null @@ -86,10 +79,6 @@ const NewQuestion = () => { !markdownText || !selectedComplexity || selectedCategories.length === 0 || - testCases.some( - (testCase) => - testCase.input.trim() === "" || testCase.expectedOutput.trim() === "" - ) || testcaseInputFile === null || testcaseOutputFile === null || Object.values(codeTemplates).some((value) => value === "") @@ -104,7 +93,6 @@ const NewQuestion = () => { description: markdownText, complexity: selectedComplexity, categories: selectedCategories, - testcases: testCases, pythonTemplate: codeTemplates.python, javaTemplate: codeTemplates.java, cTemplate: codeTemplates.c, @@ -175,11 +163,6 @@ const NewQuestion = () => { setMarkdownText={setMarkdownText} /> - - { const navigate = useNavigate(); @@ -47,7 +43,6 @@ const QuestionEdit = () => { null ); const [selectedCategories, setSelectedCategories] = useState([]); - const [testCases, setTestCases] = useState([]); const [testcaseInputFile, setTestcaseInputFile] = useState(null); const [testcaseOutputFile, setTestcaseOutputFile] = useState( null @@ -77,7 +72,6 @@ const QuestionEdit = () => { setMarkdownText(state.selectedQuestion.description); setSelectedComplexity(state.selectedQuestion.complexity); setSelectedCategories(state.selectedQuestion.categories); - setTestCases(state.selectedQuestion.testcases); setCodeTemplates({ python: state.selectedQuestion.pythonTemplate, java: state.selectedQuestion.javaTemplate, @@ -103,7 +97,6 @@ const QuestionEdit = () => { markdownText === state.selectedQuestion.description && selectedComplexity === state.selectedQuestion.complexity && selectedCategories === state.selectedQuestion.categories && - isTestcaseUnchanged(testCases, state.selectedQuestion.testcases) && codeTemplates.python === state.selectedQuestion.pythonTemplate && codeTemplates.java === state.selectedQuestion.javaTemplate && codeTemplates.c === state.selectedQuestion.cTemplate && @@ -119,10 +112,6 @@ const QuestionEdit = () => { !markdownText || !selectedComplexity || selectedCategories.length === 0 || - testCases.some( - (testCase) => - testCase.input.trim() === "" || testCase.expectedOutput.trim() === "" - ) || Object.values(codeTemplates).some((value) => value === "") ) { toast.error(FILL_ALL_FIELDS); @@ -136,7 +125,6 @@ const QuestionEdit = () => { description: markdownText, complexity: selectedComplexity, categories: selectedCategories, - testcases: testCases, testcaseInputFileUrl: state.selectedQuestion.testcaseInputFileUrl, testcaseOutputFileUrl: state.selectedQuestion.testcaseOutputFileUrl, pythonTemplate: codeTemplates.python, @@ -210,11 +198,6 @@ const QuestionEdit = () => { setMarkdownText={setMarkdownText} /> - - ; - testcases: Array; pythonTemplate: string; javaTemplate: string; cTemplate: string; @@ -178,7 +171,6 @@ export const createQuestion = async ( description: question.description, complexity: question.complexity, category: question.categories, - testcases: question.testcases, testcaseInputFileUrl, testcaseOutputFileUrl, pythonTemplate: question.pythonTemplate, @@ -317,7 +309,6 @@ export const updateQuestionById = async ( description: question.description, complexity: question.complexity, category: question.categories, - testcases: question.testcases, testcaseInputFileUrl: question.testcaseInputFileUrl, testcaseOutputFileUrl: question.testcaseOutputFileUrl, ...urls, diff --git a/frontend/src/utils/validators.ts b/frontend/src/utils/validators.ts index 9db98f55d9..bec4fdab48 100644 --- a/frontend/src/utils/validators.ts +++ b/frontend/src/utils/validators.ts @@ -1,6 +1,5 @@ /* eslint-disable */ -import { TestCase } from "../components/QuestionTestCases"; import { BIO_MAX_LENGTH_ERROR_MESSAGE, EMAIL_INVALID_ERROR_MESSAGE, @@ -120,17 +119,3 @@ export const passwordValidators = [ message: PASSWORD_SPECIAL_CHAR_ERROR_MESSAGE, }, ]; - -export const isTestcaseUnchanged = ( - newTc: Array, - oldTc: Array -) => { - return ( - newTc.length === oldTc.length && - newTc.every( - (tc, i) => - tc.input === oldTc[i].input && - tc.expectedOutput === oldTc[i].expectedOutput - ) - ); -}; From d59076fddda00c876f182175df763ae933a2f423 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 4 Nov 2024 11:05:19 +0800 Subject: [PATCH 081/192] Fix conflict errors --- frontend/src/components/Chat/index.tsx | 2 +- frontend/src/components/CodeEditor/index.tsx | 1 + frontend/src/contexts/MatchContext.tsx | 8 ++++++-- frontend/src/pages/CollabSandbox/index.tsx | 8 ++++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 8c5da246c3..4b62f7a337 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -71,7 +71,7 @@ const Chat: React.FC = ({ isActive }) => { }, []); useEffect(() => { - // initliase listerner for incoming messages + // initialize listener for incoming messages communicationSocket.on( CommunicationEvents.USER_JOINED, (message: Message) => { diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 8bab6cb8a5..66796165e0 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -69,6 +69,7 @@ const CodeEditor: React.FC = (props) => { cursorExtension(roomId, uid, username), ] : []), + EditorView.lineWrapping, EditorView.editable.of(!isReadOnly), EditorState.readOnly.of(isReadOnly), ]} diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index bac689821c..7210002499 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -81,12 +81,12 @@ type MatchContextType = { matchingTimeout: () => void; matchOfferTimeout: () => void; verifyMatchStatus: () => void; + getMatchId: () => string | null; handleEndSessionClick: () => void; handleRejectEndSession: () => void; handleConfirmEndSession: () => void; matchUser: MatchUser | null; matchCriteria: MatchCriteria | null; - matchId: string | null; partner: MatchUser | null; matchPending: boolean; loading: boolean; @@ -489,6 +489,10 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { ); }; + const getMatchId = () => { + return matchId; + }; + const handleEndSessionClick = () => { setIsEndSessionModalOpen(true); }; @@ -513,12 +517,12 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { matchingTimeout, matchOfferTimeout, verifyMatchStatus, + getMatchId, handleEndSessionClick, handleRejectEndSession, handleConfirmEndSession, matchUser, matchCriteria, - matchId, partner, matchPending, loading, diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index d4b7289ae3..cd57822547 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -66,12 +66,12 @@ const CollabSandbox: React.FC = () => { const { verifyMatchStatus, + getMatchId, handleRejectEndSession, handleConfirmEndSession, matchUser, partner, matchCriteria, - matchId, loading, isEndSessionModalOpen, questionId, @@ -93,6 +93,7 @@ const CollabSandbox: React.FC = () => { } getQuestionById(questionId, dispatch); + const matchId = getMatchId(); if (!matchUser || !matchId) { return; } @@ -135,7 +136,7 @@ const CollabSandbox: React.FC = () => { return ; } - if (!matchUser || !partner || !matchCriteria || !matchId) { + if (!matchUser || !partner || !matchCriteria || !getMatchId()) { return ; } @@ -222,7 +223,6 @@ const CollabSandbox: React.FC = () => { flex: 1, width: "100%", paddingTop: theme.spacing(2), - paddingBottom: theme.spacing(2), })} > { ? selectedQuestion.cTemplate : "" } - roomId={matchId} + roomId={getMatchId()!} /> Date: Mon, 4 Nov 2024 15:06:15 +0800 Subject: [PATCH 082/192] Update question service test and swagger --- .../src/controllers/questionController.ts | 65 ++------- .../question-service/src/models/Question.ts | 6 + .../src/models/QuestionTemplate.ts | 29 ---- backend/question-service/swagger.yml | 6 + .../tests/questionRoutes.spec.ts | 132 ++++++++++++++++++ 5 files changed, 155 insertions(+), 83 deletions(-) delete mode 100644 backend/question-service/src/models/QuestionTemplate.ts diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 56eb302d90..e8489015d1 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -1,8 +1,5 @@ import { Request, Response } from "express"; import Question, { IQuestion } from "../models/Question.ts"; -import QuestionTemplate, { - IQuestionTemplate, -} from "../models/QuestionTemplate.ts"; import { checkIsExistingQuestion, sortAlphabetically } from "../utils/utils.ts"; import { DUPLICATE_QUESTION_MESSAGE, @@ -70,22 +67,16 @@ export const createQuestion = async ( category, testcaseInputFileUrl, testcaseOutputFileUrl, - }); - - await newQuestion.save(); - - const newQuestionTemplate = new QuestionTemplate({ - questionId: newQuestion._id, pythonTemplate, javaTemplate, cTemplate, }); - await newQuestionTemplate.save(); + await newQuestion.save(); res.status(201).json({ message: QN_CREATED_MESSAGE, - question: formatQuestionIndivResponse(newQuestion, newQuestionTemplate), + question: formatQuestionIndivResponse(newQuestion), }); } catch (error) { console.log(error); @@ -195,10 +186,7 @@ export const updateQuestion = async ( try { const { id } = req.params; - const { pythonTemplate, javaTemplate, cTemplate, ...questionBody } = - req.body; - - const { title, description } = questionBody; + const { title, description } = req.body; if (!id.match(MONGO_OBJ_ID_FORMAT)) { res.status(400).json({ message: MONGO_OBJ_ID_MALFORMED_MESSAGE }); @@ -227,26 +215,13 @@ export const updateQuestion = async ( return; } - const updatedQuestion = await Question.findByIdAndUpdate(id, questionBody, { + const updatedQuestion = await Question.findByIdAndUpdate(id, req.body, { new: true, }); - const updatedQuestionTemplate = await QuestionTemplate.findOneAndUpdate( - { questionId: id }, - { - pythonTemplate, - javaTemplate, - cTemplate, - }, - { new: true }, - ); - res.status(200).json({ message: "Question updated successfully", - question: formatQuestionIndivResponse( - updatedQuestion as IQuestion, - updatedQuestionTemplate as IQuestionTemplate, - ), + question: formatQuestionIndivResponse(updatedQuestion as IQuestion), }); } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); @@ -266,7 +241,6 @@ export const deleteQuestion = async ( } await Question.findByIdAndDelete(id); - await QuestionTemplate.findOneAndDelete({ questionId: id }); res.status(200).json({ message: QN_DELETED_MESSAGE }); } catch (error) { @@ -346,16 +320,9 @@ export const readQuestionIndiv = async ( return; } - const questionTemplate = await QuestionTemplate.findOne({ - questionId: id, - }); - res.status(200).json({ message: QN_RETRIEVED_MESSAGE, - question: formatQuestionIndivResponse( - questionDetails, - questionTemplate as IQuestionTemplate, - ), + question: formatQuestionIndivResponse(questionDetails), }); } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); @@ -384,16 +351,9 @@ export const readRandomQuestion = async ( const chosenQuestion = randomQuestion[0]; - const questionTemplate = await QuestionTemplate.findOne({ - questionId: chosenQuestion._id, - }); - res.status(200).json({ message: QN_RETRIEVED_MESSAGE, - question: formatQuestionIndivResponse( - chosenQuestion, - questionTemplate as IQuestionTemplate, - ), + question: formatQuestionIndivResponse(chosenQuestion), }); } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); @@ -433,10 +393,7 @@ const formatQuestionResponse = (question: IQuestion) => { }; }; -const formatQuestionIndivResponse = ( - question: IQuestion, - questionTemplate: IQuestionTemplate, -) => { +const formatQuestionIndivResponse = (question: IQuestion) => { return { id: question._id, title: question.title, @@ -445,8 +402,8 @@ const formatQuestionIndivResponse = ( categories: question.category, testcaseInputFileUrl: question.testcaseInputFileUrl, testcaseOutputFileUrl: question.testcaseOutputFileUrl, - pythonTemplate: questionTemplate ? questionTemplate.pythonTemplate : "", - javaTemplate: questionTemplate ? questionTemplate.javaTemplate : "", - cTemplate: questionTemplate ? questionTemplate.cTemplate : "", + pythonTemplate: question.pythonTemplate, + javaTemplate: question.javaTemplate, + cTemplate: question.cTemplate, }; }; diff --git a/backend/question-service/src/models/Question.ts b/backend/question-service/src/models/Question.ts index c2dd19d491..2cbe239d58 100644 --- a/backend/question-service/src/models/Question.ts +++ b/backend/question-service/src/models/Question.ts @@ -7,6 +7,9 @@ export interface IQuestion extends Document { category: string[]; testcaseInputFileUrl: string; testcaseOutputFileUrl: string; + pythonTemplate: string; + javaTemplate: string; + cTemplate: string; createdAt: Date; updatedAt: Date; } @@ -23,6 +26,9 @@ const questionSchema: Schema = new mongoose.Schema( category: { type: [String], required: true }, testcaseInputFileUrl: { type: String, required: true }, testcaseOutputFileUrl: { type: String, required: true }, + pythonTemplate: { type: String, default: "" }, + javaTemplate: { type: String, default: "" }, + cTemplate: { type: String, default: "" }, }, { timestamps: true }, ); diff --git a/backend/question-service/src/models/QuestionTemplate.ts b/backend/question-service/src/models/QuestionTemplate.ts deleted file mode 100644 index 84cbd63097..0000000000 --- a/backend/question-service/src/models/QuestionTemplate.ts +++ /dev/null @@ -1,29 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; - -export interface IQuestionTemplate extends Document { - questionId: Types.ObjectId; - pythonTemplate: string; - javaTemplate: string; - cTemplate: string; -} - -const questionTemplateSchema: Schema = new mongoose.Schema( - { - questionId: { - type: Schema.Types.ObjectId, - ref: "Question", - required: true, - }, - pythonTemplate: { type: String, default: "" }, - javaTemplate: { type: String, default: "" }, - cTemplate: { type: String, default: "" }, - }, - { timestamps: true }, -); - -const Question = mongoose.model( - "QuestionTemplate", - questionTemplateSchema, -); - -export default Question; diff --git a/backend/question-service/swagger.yml b/backend/question-service/swagger.yml index a5cf9537eb..974226929c 100644 --- a/backend/question-service/swagger.yml +++ b/backend/question-service/swagger.yml @@ -45,6 +45,12 @@ components: items: type: string description: Categories + testcaseInputFileUrl: + type: string + description: URL of the test case input file + testcaseOutputFileUrl: + type: string + description: URL of the test case output file pythonTemplate: type: string description: Code template in Python diff --git a/backend/question-service/tests/questionRoutes.spec.ts b/backend/question-service/tests/questionRoutes.spec.ts index 5e59c20ccd..eb9500f773 100644 --- a/backend/question-service/tests/questionRoutes.spec.ts +++ b/backend/question-service/tests/questionRoutes.spec.ts @@ -105,11 +105,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.lines(); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = new Question({ title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }); await newQuestion.save(); const res = await request.get(`${BASE_URL}/${newQuestion.id}`); @@ -147,11 +158,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.lines(); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = new Question({ title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }); await newQuestion.save(); const res = await request.delete(`${BASE_URL}/${newQuestion.id}`); @@ -177,11 +199,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.lines(5); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = { title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }; const res = await request.post(`${BASE_URL}`).send(newQuestion); @@ -198,11 +231,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.lines(5); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = new Question({ title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }); await newQuestion.save(); @@ -226,11 +270,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.lines(5); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = new Question({ title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }); await newQuestion.save(); @@ -254,11 +309,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.words(QN_DESC_CHAR_LIMIT + 5); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = { title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }; const res = await request.post(`${BASE_URL}`).send(newQuestion); @@ -274,11 +340,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.lines(5); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = new Question({ title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }); await newQuestion.save(); @@ -307,11 +384,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.lines(5); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = new Question({ title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }); await newQuestion.save(); @@ -335,11 +423,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.lines(5); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = new Question({ title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }); await newQuestion.save(); @@ -365,11 +464,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.lines(5); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = new Question({ title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }); await newQuestion.save(); @@ -378,11 +488,22 @@ describe("Question routes", () => { const otherComplexity = "Medium"; const otherCategories = ["String", "Data Structures"]; const otherDescription = faker.lorem.lines(5); + const otherTestcaseInputFileUrl = faker.internet.url(); + const otherTestcaseOutputFileUrl = faker.internet.url(); + const otherPythonTemplate = "some python template"; + const otherJavaTemplate = "some java template"; + const otherCTemplate = "some c template"; + const otherQuestion = new Question({ title: otherTitle, complexity: otherComplexity, category: otherCategories, description: otherDescription, + testcaseInputFileUrl: otherTestcaseInputFileUrl, + testcaseOutputFileUrl: otherTestcaseOutputFileUrl, + pythonTemplate: otherPythonTemplate, + javaTemplate: otherJavaTemplate, + cTemplate: otherCTemplate, }); await otherQuestion.save(); @@ -407,11 +528,22 @@ describe("Question routes", () => { const complexity = "Easy"; const categories = ["Algorithms"]; const description = faker.lorem.lines(5); + const testcaseInputFileUrl = faker.internet.url(); + const testcaseOutputFileUrl = faker.internet.url(); + const pythonTemplate = "some python template"; + const javaTemplate = "some java template"; + const cTemplate = "some c template"; + const newQuestion = new Question({ title, complexity, category: categories, description, + testcaseInputFileUrl, + testcaseOutputFileUrl, + pythonTemplate, + javaTemplate, + cTemplate, }); await newQuestion.save(); From c9d5f94d322db794da4f78cc2e37e1f90484bb15 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:14:39 +0800 Subject: [PATCH 083/192] Fix lint --- .../src/controllers/codeExecutionControllers.ts | 2 +- frontend/src/components/QuestionCodeTemplates/index.tsx | 2 ++ frontend/src/reducers/qnHistoryReducer.ts | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index 6262f7d8f0..3baa5f3d4f 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -64,7 +64,7 @@ export const executeCode = async (req: Request, res: Response) => { const compilerData = (compilerResponse.data as CompilerResult[]).map( (result, index) => { - let { stdout, ...restofResult } = result; + let { stdout, ...restofResult } = result; // eslint-disable-line const expectedResultValue = expectedResultList[index].trim(); if (!stdout) { diff --git a/frontend/src/components/QuestionCodeTemplates/index.tsx b/frontend/src/components/QuestionCodeTemplates/index.tsx index c1506b6118..f153c10200 100644 --- a/frontend/src/components/QuestionCodeTemplates/index.tsx +++ b/frontend/src/components/QuestionCodeTemplates/index.tsx @@ -46,6 +46,7 @@ const QuestionCodeTemplates: React.FC = ({ })); }; + /* eslint-disable @typescript-eslint/no-explicit-any */ const handleTabKeys = (event: any) => { const { value } = event.target; @@ -65,6 +66,7 @@ const QuestionCodeTemplates: React.FC = ({ event.target.selectionEnd = cursorPosition + 1; } }; + /* eslint-enable @typescript-eslint/no-explicit-any */ return ( diff --git a/frontend/src/reducers/qnHistoryReducer.ts b/frontend/src/reducers/qnHistoryReducer.ts index 9d74707b03..9887b12adf 100644 --- a/frontend/src/reducers/qnHistoryReducer.ts +++ b/frontend/src/reducers/qnHistoryReducer.ts @@ -65,7 +65,7 @@ const isQnHistory = (qnHistory: any): qnHistory is QnHistoryDetail => { ); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any +/* eslint-disable @typescript-eslint/no-explicit-any */ const isQnHistoryList = ( qnHistoryList: any ): qnHistoryList is QnHistoryList => { @@ -75,13 +75,13 @@ const isQnHistoryList = ( return ( Array.isArray(qnHistoryList.qnHistories) && - // eslint-disable-next-line @typescript-eslint/no-explicit-any qnHistoryList.qnHistories.every((qnHistory: any) => isQnHistory(qnHistory) ) && typeof qnHistoryList.qnHistoryCount === "number" ); }; +/* eslint-enable @typescript-eslint/no-explicit-any */ export const initialQHState: QnHistoriesState = { qnHistories: [], From 4a3c861b5c1b0a52beab56029b549608f0245e57 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Mon, 4 Nov 2024 15:18:26 +0800 Subject: [PATCH 084/192] Fix some linting issues --- frontend/src/components/Chat/index.tsx | 2 +- frontend/src/pages/Home/index.tsx | 1 + frontend/src/reducers/qnHistoryReducer.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 8c5da246c3..45a20f1a2d 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -67,7 +67,7 @@ const Chat: React.FC = ({ isActive }) => { roomId: getMatchId(), username: user?.username, }); - // joinedRef.current = true; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { diff --git a/frontend/src/pages/Home/index.tsx b/frontend/src/pages/Home/index.tsx index 9ee9f3ae48..7a2a5cdfaf 100644 --- a/frontend/src/pages/Home/index.tsx +++ b/frontend/src/pages/Home/index.tsx @@ -64,6 +64,7 @@ const Home: React.FC = () => { } } setIsQueryingQnDB(false); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.questions]); if (loading) { diff --git a/frontend/src/reducers/qnHistoryReducer.ts b/frontend/src/reducers/qnHistoryReducer.ts index 9d74707b03..b480068e85 100644 --- a/frontend/src/reducers/qnHistoryReducer.ts +++ b/frontend/src/reducers/qnHistoryReducer.ts @@ -65,8 +65,8 @@ const isQnHistory = (qnHistory: any): qnHistory is QnHistoryDetail => { ); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any const isQnHistoryList = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any qnHistoryList: any ): qnHistoryList is QnHistoryList => { if (!qnHistoryList || typeof qnHistoryList !== "object") { From fff8425ce8f43f8ff6268df6d57409d57b31f2f4 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 4 Nov 2024 15:55:27 +0800 Subject: [PATCH 085/192] Integrate code editor for qns history --- .../CollabSessionControls/index.tsx | 2 + frontend/src/pages/CollabSandbox/index.tsx | 2 + .../src/pages/QuestionHistoryDetail/index.tsx | 239 +++++++++++------- frontend/src/utils/collabSocket.ts | 7 + 4 files changed, 152 insertions(+), 98 deletions(-) diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 3f261e0077..cbd3fd1299 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -8,6 +8,7 @@ import { extractMinutesFromTime, extractSecondsFromTime, } from "../../utils/sessionTime"; +import { getDocumentContent } from "../../utils/collabSocket"; const CollabSessionControls: React.FC = () => { const [time, setTime] = useState(0); @@ -45,6 +46,7 @@ const CollabSessionControls: React.FC = () => { time )} mins ${extractSecondsFromTime(time)} secs` ); + console.log(`Code: ${getDocumentContent()}`); }} // TODO: implement submit function with time taken pop-up > Submit diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index cd57822547..c1a72e3906 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -222,7 +222,9 @@ const CollabSandbox: React.FC = () => { sx={(theme) => ({ flex: 1, width: "100%", + maxHeight: "50vh", paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), })} > { const { qnHistoryId } = useParams<{ qnHistoryId: string }>(); - const [qnhistState, qnhistDispatch] = useReducer(qnHistoryReducer, initialQHState); + const [qnhistState, qnhistDispatch] = useReducer( + qnHistoryReducer, + initialQHState + ); const [qnState, qnDispatch] = useReducer(reducer, initialState); const [loading, setLoading] = useState(true); const navigate = useNavigate(); @@ -30,11 +48,14 @@ const QuestionHistoryDetail: React.FC = () => { const { user } = auth; - const tableHeaders = ["Status", "Date submitted", "Time taken", "Partner"] + const tableHeaders = ["Status", "Date submitted", "Time taken", "Partner"]; useEffect(() => { if (!qnHistoryId) { - setSelectedQnHistoryError("Unable to fetch question history.", qnhistDispatch); + setSelectedQnHistoryError( + "Unable to fetch question history.", + qnhistDispatch + ); return; } @@ -47,8 +68,7 @@ const QuestionHistoryDetail: React.FC = () => { getQuestionById(qnhistState.selectedQnHistory.questionId, qnDispatch); } setTimeout(() => setLoading(false), 500); - }, [qnhistState]) - + }, [qnhistState]); const getPartnerId = (userIds: string[], currUserId: string): string => { if (currUserId == userIds[0]) { @@ -56,12 +76,12 @@ const QuestionHistoryDetail: React.FC = () => { } else { return userIds[0]; } - } + }; if (loading) { return ; } - + if (!qnhistState.selectedQnHistory) { if (qnhistState.selectedQnHistoryError) { return ( @@ -75,116 +95,139 @@ const QuestionHistoryDetail: React.FC = () => { } } - const partnerId = user && qnhistState.selectedQnHistory && getPartnerId(qnhistState.selectedQnHistory.userIds, user.id); + const partnerId = + user && + qnhistState.selectedQnHistory && + getPartnerId(qnhistState.selectedQnHistory.userIds, user.id); return ( - navigate(`/profile/${user?.id}`)}> + navigate(`/profile/${user?.id}`)} + > - Latest submission details - { user && qnhistState.selectedQnHistory && - -

({ - "& .MuiTableCell-root": { padding: theme.spacing(1.2) }, - whiteSpace: "nowrap", - })} - > - - - {tableHeaders.map((header) => ( - - - {header} + + Latest submission details + + {user && qnhistState.selectedQnHistory && ( + +
({ + "& .MuiTableCell-root": { padding: theme.spacing(1.2) }, + whiteSpace: "nowrap", + })} + > + + + {tableHeaders.map((header) => ( + + + {header} + + + ))} + + + + + + + {qnhistState.selectedQnHistory.submissionStatus} - ))} - - - - - - - {qnhistState.selectedQnHistory.submissionStatus} - - - - - {convertDateString(qnhistState.selectedQnHistory.dateAttempted)} - - - - + {convertDateString( + qnhistState.selectedQnHistory.dateAttempted + )} + + + - {`${qnhistState.selectedQnHistory.timeTaken} mins`} - - - - + {`${qnhistState.selectedQnHistory.timeTaken} mins`} + + + navigate(`/profile/${partnerId}`)} > - {"Go to partner profile"} - - - - -
-
- } + navigate(`/profile/${partnerId}`)} + > + {"Go to partner profile"} + + + + + + + )} ({ flex: 1, marginRight: theme.spacing(2) })}> - {qnState.selectedQuestion - ? - : - } + {qnState.selectedQuestion ? ( + + ) : ( + + )} ({ flex: 1, marginLeft: theme.spacing(2) })}> - Code editor + ({ + flex: 1, + width: "100%", + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + })} + > + + diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 8785efb7c3..1b0f035ac1 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -127,3 +127,10 @@ const initConnectionStatusListeners = (roomId: string) => { }); } }; + +export const getDocumentContent = () => { + if (!doc.isDestroyed) { + return text.toString(); + } + return ""; +}; From 5bdbf9aa107271474ac73c50c99bed661dca9e4d Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Mon, 4 Nov 2024 16:23:55 +0800 Subject: [PATCH 086/192] Fix linting issue --- frontend/src/components/QuestionCodeTemplates/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/QuestionCodeTemplates/index.tsx b/frontend/src/components/QuestionCodeTemplates/index.tsx index c1506b6118..b5ee81fa3b 100644 --- a/frontend/src/components/QuestionCodeTemplates/index.tsx +++ b/frontend/src/components/QuestionCodeTemplates/index.tsx @@ -46,6 +46,7 @@ const QuestionCodeTemplates: React.FC = ({ })); }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleTabKeys = (event: any) => { const { value } = event.target; From a3d87cac4f6c25eb1bd13754421d417ae075a982 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:25:36 +0800 Subject: [PATCH 087/192] Update to triple line instead --- backend/code-execution-service/src/utils/testCasesApi.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/code-execution-service/src/utils/testCasesApi.ts b/backend/code-execution-service/src/utils/testCasesApi.ts index 807728fa0e..475dec3e67 100644 --- a/backend/code-execution-service/src/utils/testCasesApi.ts +++ b/backend/code-execution-service/src/utils/testCasesApi.ts @@ -7,9 +7,9 @@ export const testCasesApi = async ( const inputFileUrlResponse = await axios.get(inputFileUrl); const outputFileUrlResponse = await axios.get(outputFileUrl); - // Split the input and output files by double new line + // Split the input and output files by triple new line return { - input: inputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"), - output: outputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"), + input: inputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n\n"), + output: outputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n\n"), }; }; From 60889b495d5317357367985a9448f4187227eda2 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:39:40 +0800 Subject: [PATCH 088/192] Update frontend tooltip message for file input --- .../QuestionTestCasesFileUpload/index.tsx | 16 +++++++++++++--- frontend/src/utils/constants.ts | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/QuestionTestCasesFileUpload/index.tsx b/frontend/src/components/QuestionTestCasesFileUpload/index.tsx index f2963ea9e1..070c83b0e7 100644 --- a/frontend/src/components/QuestionTestCasesFileUpload/index.tsx +++ b/frontend/src/components/QuestionTestCasesFileUpload/index.tsx @@ -1,7 +1,9 @@ -import { Box, IconButton, Stack, Tooltip, Typography } from "@mui/material"; +import { Box, IconButton, Stack, Typography } from "@mui/material"; +import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip"; import { HelpOutlined } from "@mui/icons-material"; import QuestionFileContainer from "../QuestionFileContainer"; import { ADD_TEST_CASE_FILES_TOOLTIP_MESSAGE } from "../../utils/constants"; +import { styled } from "@mui/material/styles"; interface QuestionTestCasesFileUploadProps { testcaseInputFile: File | null; @@ -10,6 +12,14 @@ interface QuestionTestCasesFileUploadProps { setTestcaseOutputFile: React.Dispatch>; } +const CustomWidthTooltip = styled(({ className, ...props }: TooltipProps) => ( + +))({ + [`& .${tooltipClasses.tooltip}`]: { + maxWidth: 400, + }, +}); + const QuestionTestCasesFileUpload: React.FC< QuestionTestCasesFileUploadProps > = ({ @@ -22,7 +32,7 @@ const QuestionTestCasesFileUpload: React.FC< Test Cases File Upload - - + diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 58049e2200..eeecfef669 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -116,7 +116,7 @@ export const COLLABORATIVE_EDITOR_PATH = "/collaborative_editor.png"; /* Tooltips */ export const ADD_QUESTION_TEST_CASE_TOOLTIP_MESSAGE = `Add at least 1 and at most 3 test cases.
This will be displayed to users.`; -export const ADD_TEST_CASE_FILES_TOOLTIP_MESSAGE = `Upload files for executing test cases when the user submits code.

This is a required field. Only text files are accepted.

Please ensure that each test case in the file is separated by a double newline.

For example, if you want to find the sum of four input numbers, an input file with 2 test cases could look like:
"""
1
2
3
4

5
6
7
8
"""

The corresponding output file, with each result in a single line, should look like:
"""
10

26
"""

Each output line is the sum of the four numbers in the respective test case.`; +export const ADD_TEST_CASE_FILES_TOOLTIP_MESSAGE = `Upload files for executing test cases when the user submits code.

This is a required field. Only text files are accepted.

Please ensure that each test case in the file is separated by a triple newline.

For example, if you want to find the sum of four input numbers, an input file with 2 test cases could look like:
"""
1
2
3
4


5
6
7
8
"""

The corresponding output file, with each result in a single line, should look like:
"""
10


26
"""

Each output line is the sum of the four numbers in the respective test case.`; export const CODE_TEMPLATES_TOOLTIP_MESSAGE = `This is a required field.
Fill in a code template for each language.`; /* Code Templates */ From 551331dae82e22cf1fe0007e71717ce71169b35a Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Mon, 4 Nov 2024 17:29:07 +0800 Subject: [PATCH 089/192] Display test cases --- .../src/controllers/questionController.ts | 32 ++++++++++++----- backend/question-service/src/utils/utils.ts | 12 ++++++- frontend/src/components/TestCase/index.tsx | 26 ++++++++------ frontend/src/pages/CollabSandbox/index.tsx | 35 ++++--------------- frontend/src/reducers/questionReducer.ts | 27 +++++++------- 5 files changed, 68 insertions(+), 64 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 56eb302d90..da2c0e88a3 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -3,7 +3,11 @@ import Question, { IQuestion } from "../models/Question.ts"; import QuestionTemplate, { IQuestionTemplate, } from "../models/QuestionTemplate.ts"; -import { checkIsExistingQuestion, sortAlphabetically } from "../utils/utils.ts"; +import { + checkIsExistingQuestion, + getFileContent, + sortAlphabetically, +} from "../utils/utils.ts"; import { DUPLICATE_QUESTION_MESSAGE, QN_DESC_EXCEED_CHAR_LIMIT_MESSAGE, @@ -85,7 +89,10 @@ export const createQuestion = async ( res.status(201).json({ message: QN_CREATED_MESSAGE, - question: formatQuestionIndivResponse(newQuestion, newQuestionTemplate), + question: await formatQuestionIndivResponse( + newQuestion, + newQuestionTemplate, + ), }); } catch (error) { console.log(error); @@ -243,7 +250,7 @@ export const updateQuestion = async ( res.status(200).json({ message: "Question updated successfully", - question: formatQuestionIndivResponse( + question: await formatQuestionIndivResponse( updatedQuestion as IQuestion, updatedQuestionTemplate as IQuestionTemplate, ), @@ -352,7 +359,7 @@ export const readQuestionIndiv = async ( res.status(200).json({ message: QN_RETRIEVED_MESSAGE, - question: formatQuestionIndivResponse( + question: await formatQuestionIndivResponse( questionDetails, questionTemplate as IQuestionTemplate, ), @@ -390,7 +397,7 @@ export const readRandomQuestion = async ( res.status(200).json({ message: QN_RETRIEVED_MESSAGE, - question: formatQuestionIndivResponse( + question: await formatQuestionIndivResponse( chosenQuestion, questionTemplate as IQuestionTemplate, ), @@ -433,18 +440,27 @@ const formatQuestionResponse = (question: IQuestion) => { }; }; -const formatQuestionIndivResponse = ( +const formatQuestionIndivResponse = async ( question: IQuestion, questionTemplate: IQuestionTemplate, ) => { + const testcaseDelimiter = "\n"; + const inputs = (await getFileContent(question.testcaseInputFileUrl)).split( + testcaseDelimiter, + ); + const outputs = (await getFileContent(question.testcaseOutputFileUrl)).split( + testcaseDelimiter, + ); return { id: question._id, title: question.title, description: question.description, complexity: question.complexity, categories: question.category, - testcaseInputFileUrl: question.testcaseInputFileUrl, - testcaseOutputFileUrl: question.testcaseOutputFileUrl, + // testcaseInputFileUrl: question.testcaseInputFileUrl, + // testcaseOutputFileUrl: question.testcaseOutputFileUrl, + inputs, + outputs, pythonTemplate: questionTemplate ? questionTemplate.pythonTemplate : "", javaTemplate: questionTemplate ? questionTemplate.javaTemplate : "", cTemplate: questionTemplate ? questionTemplate.cTemplate : "", diff --git a/backend/question-service/src/utils/utils.ts b/backend/question-service/src/utils/utils.ts index 4bccc53823..c001266139 100644 --- a/backend/question-service/src/utils/utils.ts +++ b/backend/question-service/src/utils/utils.ts @@ -1,8 +1,8 @@ +import axios from "axios"; import mongoose from "mongoose"; import { v4 as uuidv4 } from "uuid"; import { bucket } from "../config/firebase"; - import Question from "../models/Question"; export const checkIsExistingQuestion = async ( @@ -50,6 +50,16 @@ export const uploadFileToFirebase = async ( }); }; +export const getFileContent = async (url: string): Promise => { + try { + const { data } = await axios.get(url); + return data; + } catch (error) { + console.error(error); + return ""; + } +}; + export const sortAlphabetically = (arr: string[]) => { return [...arr].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }), diff --git a/frontend/src/components/TestCase/index.tsx b/frontend/src/components/TestCase/index.tsx index 424eee0004..a430d67225 100644 --- a/frontend/src/components/TestCase/index.tsx +++ b/frontend/src/components/TestCase/index.tsx @@ -2,9 +2,9 @@ import { Box, styled, Typography } from "@mui/material"; type TestCaseProps = { input: string; - output: string; + output?: string; stdout: string; - result: string; + result?: string; }; const StyledBox = styled(Box)(({ theme }) => ({ @@ -30,16 +30,20 @@ const TestCase: React.FC = ({ Input {input} + {output && ( + + Output + {output} + + )} + {stdout && ( + + Stdout + {stdout} + + )} - Output - {output} - - - Standard output - {stdout} - - - Result + Expected {result}
diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 9d2bf9afb4..fd89b7e808 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -27,30 +27,6 @@ import Chat from "../../components/Chat"; import TabPanel from "../../components/TabPanel"; import TestCase from "../../components/TestCase"; -// hardcode for now... - -type TestCase = { - input: string; - output: string; - stdout: string; - result: string; -}; - -const testcases: TestCase[] = [ - { - input: "1 2 3 4", - output: "1 2 3 4", - stdout: "1\n2\n3\n4", - result: "1 2 3 4", - }, - { - input: "5 6 7 8", - output: "5 6 7 8", - stdout: "5\n6\n7\n8", - result: "5 6 7 8", - }, -]; - const CollabSandbox: React.FC = () => { const [showErrorScreen, setShowErrorScreen] = useState(false); @@ -219,7 +195,7 @@ const CollabSandbox: React.FC = () => { ({ margin: theme.spacing(2, 0) })}> - {[...Array(testcases.length)] + {[...Array(selectedQuestion.inputs.length)] .map((_, index) => index + 1) .map((i) => ( ))} + {/* display result of each test case in the output (result) and stdout (any print statements executed) */} diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts index 69f38025d0..25a5471e07 100644 --- a/frontend/src/reducers/questionReducer.ts +++ b/frontend/src/reducers/questionReducer.ts @@ -18,15 +18,17 @@ type QuestionDetail = { description: string; complexity: string; categories: Array; + inputs: Array; + outputs: Array; pythonTemplate: string; javaTemplate: string; cTemplate: string; }; -type QuestionDetailWithUrl = QuestionDetail & { - testcaseInputFileUrl: string; - testcaseOutputFileUrl: string; -}; +// type QuestionDetailWithUrl = QuestionDetail & { +// testcaseInputFileUrl: string; +// testcaseOutputFileUrl: string; +// }; type QuestionListDetail = { id: string; @@ -56,26 +58,21 @@ enum QuestionActionTypes { type QuestionActions = { type: QuestionActionTypes; - payload: - | QuestionList - | QuestionDetail - | QuestionDetailWithUrl - | string[] - | string; + payload: QuestionList | QuestionDetail | QuestionDetail | string[] | string; }; type QuestionsState = { questionCategories: Array; questions: Array; questionCount: number; - selectedQuestion: QuestionDetailWithUrl | null; + selectedQuestion: QuestionDetail | null; questionCategoriesError: string | null; questionListError: string | null; selectedQuestionError: string | null; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const isQuestion = (question: any): question is QuestionDetailWithUrl => { +const isQuestion = (question: any): question is QuestionDetail => { if (!question || typeof question !== "object") { return false; } @@ -272,7 +269,7 @@ export const getQuestionById = ( export const updateQuestionById = async ( questionId: string, - question: Omit, + question: Omit, testcaseFiles: TestcaseFiles, dispatch: Dispatch ): Promise => { @@ -309,8 +306,8 @@ export const updateQuestionById = async ( description: question.description, complexity: question.complexity, category: question.categories, - testcaseInputFileUrl: question.testcaseInputFileUrl, - testcaseOutputFileUrl: question.testcaseOutputFileUrl, + // testcaseInputFileUrl: question.testcaseInputFileUrl, + // testcaseOutputFileUrl: question.testcaseOutputFileUrl, ...urls, pythonTemplate: question.pythonTemplate, javaTemplate: question.javaTemplate, From aec66253201fa7d0d3f2a837fd8fc3d4141de66c Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:41:17 +0800 Subject: [PATCH 090/192] Update tooltip --- .../src/utils/testCasesApi.ts | 6 +++--- .../QuestionTestCasesFileUpload/index.tsx | 2 +- frontend/src/utils/constants.ts | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/backend/code-execution-service/src/utils/testCasesApi.ts b/backend/code-execution-service/src/utils/testCasesApi.ts index 475dec3e67..807728fa0e 100644 --- a/backend/code-execution-service/src/utils/testCasesApi.ts +++ b/backend/code-execution-service/src/utils/testCasesApi.ts @@ -7,9 +7,9 @@ export const testCasesApi = async ( const inputFileUrlResponse = await axios.get(inputFileUrl); const outputFileUrlResponse = await axios.get(outputFileUrl); - // Split the input and output files by triple new line + // Split the input and output files by double new line return { - input: inputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n\n"), - output: outputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n\n"), + input: inputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"), + output: outputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"), }; }; diff --git a/frontend/src/components/QuestionTestCasesFileUpload/index.tsx b/frontend/src/components/QuestionTestCasesFileUpload/index.tsx index 070c83b0e7..d5f9df229c 100644 --- a/frontend/src/components/QuestionTestCasesFileUpload/index.tsx +++ b/frontend/src/components/QuestionTestCasesFileUpload/index.tsx @@ -16,7 +16,7 @@ const CustomWidthTooltip = styled(({ className, ...props }: TooltipProps) => ( ))({ [`& .${tooltipClasses.tooltip}`]: { - maxWidth: 400, + maxWidth: 500, }, }); diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index eeecfef669..c570422c98 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -116,7 +116,21 @@ export const COLLABORATIVE_EDITOR_PATH = "/collaborative_editor.png"; /* Tooltips */ export const ADD_QUESTION_TEST_CASE_TOOLTIP_MESSAGE = `Add at least 1 and at most 3 test cases.
This will be displayed to users.`; -export const ADD_TEST_CASE_FILES_TOOLTIP_MESSAGE = `Upload files for executing test cases when the user submits code.

This is a required field. Only text files are accepted.

Please ensure that each test case in the file is separated by a triple newline.

For example, if you want to find the sum of four input numbers, an input file with 2 test cases could look like:
"""
1
2
3
4


5
6
7
8
"""

The corresponding output file, with each result in a single line, should look like:
"""
10


26
"""

Each output line is the sum of the four numbers in the respective test case.`; +export const ADD_TEST_CASE_FILES_TOOLTIP_MESSAGE = ` + Upload files for executing test cases when the user submits code.

+ This is a required field. Only text files are accepted.

+ Please ensure that each test case in the file is separated by a double newline.

+ For example, if the question is "Two Sum", an input file with 2 test cases could look like:
+ """ +
2 7 11 15
9

3 2 4
6
+ """

+ The first line of each test case is the input array, while the second line is the target value.

+ The corresponding output file, with each result in a single line, should look like:
+ """ +
0 1

1 2
+ """

+ Each line in the output file represents the indices of the two numbers that add up to the target for each respective test case. +`; export const CODE_TEMPLATES_TOOLTIP_MESSAGE = `This is a required field.
Fill in a code template for each language.`; /* Code Templates */ From 4724e7fc21bd9f15cf681962b770917b726f64d4 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:28:18 +0800 Subject: [PATCH 091/192] Seed question --- backend/question-service/src/scripts/seed.ts | 145 ++++++++++-------- .../testcases/longestSubstringInput.txt | 5 + .../testcases/longestSubstringOutput.txt | 5 + .../testcases/medianTwoSortedArrayInput.txt | 5 + .../testcases/medianTwoSortedArrayOutput.txt | 3 + .../src/scripts/testcases/twoSumInput.txt | 5 + .../src/scripts/testcases/twoSumOutput.txt | 3 + 7 files changed, 104 insertions(+), 67 deletions(-) create mode 100644 backend/question-service/src/scripts/testcases/longestSubstringInput.txt create mode 100644 backend/question-service/src/scripts/testcases/longestSubstringOutput.txt create mode 100644 backend/question-service/src/scripts/testcases/medianTwoSortedArrayInput.txt create mode 100644 backend/question-service/src/scripts/testcases/medianTwoSortedArrayOutput.txt create mode 100644 backend/question-service/src/scripts/testcases/twoSumInput.txt create mode 100644 backend/question-service/src/scripts/testcases/twoSumOutput.txt diff --git a/backend/question-service/src/scripts/seed.ts b/backend/question-service/src/scripts/seed.ts index a2cf05e2f7..44fe8bc81f 100644 --- a/backend/question-service/src/scripts/seed.ts +++ b/backend/question-service/src/scripts/seed.ts @@ -1,24 +1,51 @@ import { exit } from "process"; import connectDB from "../config/db"; import Question from "../models/Question"; +import { uploadFileToFirebase } from "../utils/utils"; +import { readFile } from "fs/promises"; +import path from "path"; +import { Readable } from "stream"; + +async function readTestCaseFile(filePath: string) { + try { + const absolutePath = path.resolve(filePath); + const buffer = await readFile(absolutePath); + + const file = { + fieldname: "file", + originalname: path.basename(filePath), + encoding: "7bit", + mimetype: "text/plain", + buffer: buffer, + size: buffer.length, + destination: "", + filename: "", + path: filePath, + stream: Readable.from(buffer), + }; + + const fileUrl = await uploadFileToFirebase(file, "testcaseFiles/"); + return fileUrl; + } catch (error) { + console.error(`Error reading or uploading file from ${filePath}`, error); + } +} export async function seedQuestions() { await connectDB(); const questions = [ - { - title: "Serialize and Deserialize Binary Tree", - description: - "Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure. \n\n![image](https://firebasestorage.googleapis.com/v0/b/peerprep-c3bd1.appspot.com/o/07148757-21b2-4c20-93e0-d8bef1b3560d?alt=media)", - complexity: "Hard", - category: ["Tree"], - }, { title: "Two Sum", description: - "Given an array of integers `nums` and an integer `target`, return indices of the two numbers such that they add up to `target`. You may assume that each input would have **exactly one solution**, and you may not use the same element twice. You can return the answer in any order.", + "Given an array of integers `nums` and an integer `target`, return indices of the two numbers in a list such that they add up to `target`. You may assume that each input would have **exactly one solution**, and you may not use the same element twice. You can return the answer in any order.", complexity: "Easy", category: ["Arrays"], + testcaseInputFileUrl: "./src/scripts/testcases/twoSumInput.txt", + testcaseOutputFileUrl: "./src/scripts/testcases/twoSumOutput.txt", + pythonTemplate: `# Please do not modify the main function\ndef main():\n\tprint(" ".join(solution()))\n\n\n# Write your code here\ndef solution():\n\treturn []\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `public class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n System.out.println(String.join(" ", solution()));\n }\n\n // Write your code here\n public static String[] solution() {\n return new String[]{};\n }\n}`, + cTemplate: `#include \n\n// Function to implement\nconst char** solution() {\n static const char* result[] = {NULL}; // Placeholder\n return result;\n}\n\n// Please do not modify the main function\nint main() {\n const char** result = solution();\n for (int i = 0; result[i] != NULL; i++) {\n printf("%s ", result[i]);\n }\n printf("\\n");\n return 0;\n}`, }, { title: "Longest Substring Without Repeating Characters", @@ -26,69 +53,43 @@ export async function seedQuestions() { "Given a string `s`, find the length of the **longest substring** without repeating characters.", complexity: "Medium", category: ["Strings"], + testcaseInputFileUrl: "./src/scripts/testcases/longestSubstringInput.txt", + testcaseOutputFileUrl: + "./src/scripts/testcases/longestSubstringOutput.txt", + pythonTemplate: `# Please do not modify the main function\ndef main():\n\ts = input().strip()\n\tprint(solution(s))\n\n\n# Write your code here\ndef solution(s):\n\t# Implement your solution here\n\treturn 0\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `import java.util.Scanner;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n String s = scanner.nextLine().trim();\n System.out.println(solution(s));\n }\n\n // Write your code here\n public static int solution(String s) {\n // Implement your solution here\n return 0;\n }\n}`, + cTemplate: `#include \n#include \n\n// Function to implement\nint solution(const char* s) {\n // Implement your solution here\n return 0;\n}\n\n// Please do not modify the main function\nint main() {\n char s[1000];\n fgets(s, sizeof(s), stdin);\n // Remove newline from input if exists\n s[strcspn(s, "\\n")] = 0;\n printf("%d\\n", solution(s));\n return 0;\n}`, }, { title: "Median of Two Sorted Arrays", description: - "Given two sorted arrays `nums1` and `nums2` of size `m` and `n` respectively, return the median of the two sorted arrays.", - complexity: "Hard", - category: ["Arrays"], - }, - { - title: "Longest Palindromic Substring", - description: - "Given a string `s`, return the **longest palindromic substring** in `s`.", - complexity: "Medium", - category: ["Strings", "Dynamic Programming"], - }, - { - title: "ZigZag Conversion", - description: - "The string `PAYPALISHIRING` is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility) P A H N A P L S I I G Y I R And then read line by line: `PAHNAPLSIIGYIR` Write the code that will take a string and make this conversion given a number of rows.", - complexity: "Medium", - category: ["Strings"], - }, - { - title: "Reverse Integer", - description: - "Given a signed 32-bit integer `x`, return `x` with its digits reversed. If reversing `x` causes the value to go outside the signed 32-bit integer range `[-2^31, 2^31 - 1]`, then return 0.", - complexity: "Easy", - category: ["Strings"], - }, - { - title: "String to Integer (atoi)", - description: - "Implement the `myAtoi(string s)` function, which converts a string to a 32-bit signed integer (similar to C/C++'s `atoi` function).", - complexity: "Medium", - category: ["Strings"], - }, - { - title: "Regular Expression Matching", - description: - "Given an input string `s` and a pattern `p`, implement regular expression matching with support for `'.'` and `'*'` where: - `'.'` Matches any single character.​​​​ - `'*'` Matches zero or more of the preceding element.", + "Given two sorted arrays `nums1` and `nums2` of size `m` and `n` respectively, return the median of the two sorted arrays. Round your answer to 1 decimal place.\n\n" + + "Each test case consists of two lines:\n" + + "- The first line contains the elements of `nums1`, a sorted array of integers.\n" + + "- The second line contains the elements of `nums2`, another sorted array of integers.\n\n" + + "Test cases are separated by a double newline. For example, an input file with two test cases could look like:\n" + + "```\n" + + "1 3\n2\n\n1 2\n3 4\n" + + "```\n\n" + + "### Output\n" + + "For each test case, output a single line containing the median of the two sorted arrays. Results should be separated by a double newline.\n\n" + + "The corresponding output file for the example above would be:\n" + + "```\n" + + "2.0\n\n2.5\n" + + "```\n\n" + + "### Explanation\n" + + "- **Test Case 1**: `nums1 = [1, 3]` and `nums2 = [2]` have a combined sorted array `[1, 2, 3]`, with median `2.0`.\n" + + "- **Test Case 2**: `nums1 = [1, 2]` and `nums2 = [3, 4]` have a combined sorted array `[1, 2, 3, 4]`, with median `2.5`.", + complexity: "Hard", - category: ["Strings", "Dynamic Programming"], - }, - { - title: "Container With Most Water", - description: - "Given `n` non-negative integers `a1, a2, ..., an`, where each represents a point at coordinate `(i, ai)`. `n` vertical lines are drawn such that the two endpoints of the line `i` is at `(i, ai)` and `(i, 0)`. Find two lines, which, together with the x-axis forms a container, such that the container contains the most water.", - complexity: "Medium", category: ["Arrays"], - }, - { - title: "Integer to Roman", - description: - "Roman numerals are represented by seven different symbols: `I`, `V`, `X`, `L`, `C`, `D` and `M`. Given an integer, convert it to a roman numeral.", - complexity: "Medium", - category: ["Strings"], - }, - { - title: "Roman to Integer", - description: - "Roman numerals are represented by seven different symbols: `I`, `V`, `X`, `L`, `C`, `D` and `M`. Given a roman numeral, convert it to an integer.", - complexity: "Easy", - category: ["Strings"], + testcaseInputFileUrl: + "./src/scripts/testcases/medianTwoSortedArrayInput.txt", + testcaseOutputFileUrl: + "./src/scripts/testcases/medianTwoSortedArrayOutput.txt", + pythonTemplate: `# Please do not modify the main function\ndef main():\n\timport sys\n\tinput = sys.stdin.read().strip().split("\\n\\n")\n\tfor case in input:\n\t\tlines = case.split("\\n")\n\t\tnums1 = list(map(int, lines[0].split()))\n\t\tnums2 = list(map(int, lines[1].split()))\n\t\tprint(solution(nums1, nums2))\n\n\n# Write your code here\ndef solution(nums1, nums2):\n\t# Implement your solution here\n\treturn 0.0\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `import java.util.*;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n List nums1 = new ArrayList<>();\n List nums2 = new ArrayList<>();\n boolean isNums2 = false;\n while (scanner.hasNextLine()) {\n String line = scanner.nextLine().trim();\n if (line.isEmpty()) {\n isNums2 = !isNums2;\n continue;\n }\n List nums = isNums2 ? nums2 : nums1;\n for (String num : line.split(" ")) {\n nums.add(Integer.parseInt(num));\n }\n }\n System.out.println(solution(nums1.stream().mapToInt(i -> i).toArray(), nums2.stream().mapToInt(i -> i).toArray()));\n }\n\n // Write your code here\n public static double solution(int[] nums1, int[] nums2) {\n // Implement your solution here\n return 0.0;\n }\n}`, + cTemplate: `#include \n#include \n\n// Function to implement\ndouble solution(int* nums1, int nums1Size, int* nums2, int nums2Size) {\n // Implement your solution here\n return 0.0;\n}\n\n// Please do not modify the main function\nint main() {\n int nums1[100], nums2[100], n1 = 0, n2 = 0;\n char line[1000];\n while (fgets(line, sizeof(line), stdin)) {\n if (line[0] == '\\n') break;\n char* token = strtok(line, \" \");\n while (token) {\n nums1[n1++] = atoi(token);\n token = strtok(NULL, \" \");\n }\n }\n while (fgets(line, sizeof(line), stdin)) {\n if (line[0] == '\\n') break;\n char* token = strtok(line, \" \");\n while (token) {\n nums2[n2++] = atoi(token);\n token = strtok(NULL, \" \");\n }\n }\n printf(\"%.1f\\n\", solution(nums1, n1, nums2, n2));\n return 0;\n}`, }, ]; @@ -98,10 +99,20 @@ export async function seedQuestions() { if (existingQn) { continue; } - await Question.create(qn); + + const inputUrl = await readTestCaseFile(qn.testcaseInputFileUrl); + const outputUrl = await readTestCaseFile(qn.testcaseOutputFileUrl); + + const question = await Question.create({ + ...qn, + testcaseInputFileUrl: inputUrl, + testcaseOutputFileUrl: outputUrl, + }); + console.log(`Question seeded: ${question._id}`); } console.log("Questions seeded successfully."); - } catch { + } catch (err) { + console.log(err); console.error("Error creating questions."); } exit(); diff --git a/backend/question-service/src/scripts/testcases/longestSubstringInput.txt b/backend/question-service/src/scripts/testcases/longestSubstringInput.txt new file mode 100644 index 0000000000..12fb5dc5e5 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/longestSubstringInput.txt @@ -0,0 +1,5 @@ +abcabcbb + +bbbbb + +pwwkew \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/longestSubstringOutput.txt b/backend/question-service/src/scripts/testcases/longestSubstringOutput.txt new file mode 100644 index 0000000000..44fa5a298b --- /dev/null +++ b/backend/question-service/src/scripts/testcases/longestSubstringOutput.txt @@ -0,0 +1,5 @@ +3 + +1 + +3 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/medianTwoSortedArrayInput.txt b/backend/question-service/src/scripts/testcases/medianTwoSortedArrayInput.txt new file mode 100644 index 0000000000..cc5edd0304 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/medianTwoSortedArrayInput.txt @@ -0,0 +1,5 @@ +1 3 +2 + +1 2 +3 4 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/medianTwoSortedArrayOutput.txt b/backend/question-service/src/scripts/testcases/medianTwoSortedArrayOutput.txt new file mode 100644 index 0000000000..e9f3b679e3 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/medianTwoSortedArrayOutput.txt @@ -0,0 +1,3 @@ +2.0 + +2.5 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/twoSumInput.txt b/backend/question-service/src/scripts/testcases/twoSumInput.txt new file mode 100644 index 0000000000..fca5f0f46a --- /dev/null +++ b/backend/question-service/src/scripts/testcases/twoSumInput.txt @@ -0,0 +1,5 @@ +2 7 11 15 +9 + +3 2 4 +6 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/twoSumOutput.txt b/backend/question-service/src/scripts/testcases/twoSumOutput.txt new file mode 100644 index 0000000000..a7b3114db7 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/twoSumOutput.txt @@ -0,0 +1,3 @@ +0 1 + +1 2 \ No newline at end of file From 14b02992f914612e5a52950a2768641320960fae Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 4 Nov 2024 23:01:11 +0800 Subject: [PATCH 092/192] Fix code template initialization --- .../src/handlers/websocketHandler.ts | 6 +++- frontend/src/components/CodeEditor/index.tsx | 34 +++++++++++-------- frontend/src/utils/collabCursor.ts | 2 +- frontend/src/utils/collabSocket.ts | 10 +++++- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 062cd1526d..1c98b93305 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -15,6 +15,7 @@ enum CollabEvents { // Send ROOM_READY = "room_ready", + DOCUMENT_READY = "document_ready", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", // PARTNER_LEFT = "partner_left", @@ -60,7 +61,10 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { const isPartnerReady = partnerReadiness.get(roomId); if (isPartnerReady && doc.getText().length === 0) { - doc.getText().insert(0, template); + doc.transact(() => { + doc.getText().insert(0, template); + }); + io.to(roomId).emit(CollabEvents.DOCUMENT_READY); } else { partnerReadiness.set(roomId, true); } diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 66796165e0..77f4910a00 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -1,9 +1,9 @@ -import CodeMirror from "@uiw/react-codemirror"; +import CodeMirror, { ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { langs } from "@uiw/codemirror-extensions-langs"; import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; import { EditorView } from "@codemirror/view"; import { EditorState } from "@codemirror/state"; -import { useEffect, useRef } from "react"; +import { useEffect, useState } from "react"; import { initDocument } from "../../utils/collabSocket"; import { cursorExtension } from "../../utils/collabCursor"; import { yCollab } from "y-codemirror.next"; @@ -37,24 +37,30 @@ const CodeEditor: React.FC = (props) => { isReadOnly = false, } = props; - const effectRan = useRef(false); + const [isEditorReady, setIsEditorReady] = useState(false); + const [isDocumentLoaded, setIsDocumentLoaded] = useState(false); - useEffect(() => { - if (isReadOnly) { - return; + const onEditorReady = (editor: ReactCodeMirrorRef) => { + if (!isEditorReady && editor?.editor && editor?.state && editor?.view) { + setIsEditorReady(true); } + }; - if (!effectRan.current) { - initDocument(roomId, template); + useEffect(() => { + if (isReadOnly || !isEditorReady) { + return; } - return () => { - effectRan.current = true; + const loadTemplate = async () => { + await initDocument(uid, roomId, template); + setIsDocumentLoaded(true); }; - }, []); + loadTemplate(); + }, [isReadOnly, isEditorReady]); return ( = (props) => { ] : []), EditorView.lineWrapping, - EditorView.editable.of(!isReadOnly), - EditorState.readOnly.of(isReadOnly), + EditorView.editable.of(!isReadOnly && isDocumentLoaded), + EditorState.readOnly.of(isReadOnly || !isDocumentLoaded), ]} - value={isReadOnly ? template : undefined} + value={isReadOnly ? template : template ? "Loading code template..." : ""} /> ); }; diff --git a/frontend/src/utils/collabCursor.ts b/frontend/src/utils/collabCursor.ts index 2cfa18b1da..5de6bbf76c 100644 --- a/frontend/src/utils/collabCursor.ts +++ b/frontend/src/utils/collabCursor.ts @@ -66,7 +66,6 @@ const cursorStateField = (uid: string): StateField => { for (const effect of transaction.effects) { // check for partner's cursor updates if (effect.is(updateCursor) && effect.value.uid !== uid) { - // if (effect.is(updateCursor)) { const cursorUpdates = []; if (effect.value.from !== effect.value.to) { @@ -121,6 +120,7 @@ const cursorBaseTheme = EditorView.baseTheme({ position: "absolute", marginTop: "-35px", marginLeft: "0px", + whiteSpace: "nowrap", }, ".cm-cursor-color": { backgroundColor: "#f6a1a1", diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 1b0f035ac1..f52bc24819 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -15,6 +15,7 @@ enum CollabEvents { // Receive ROOM_READY = "room_ready", + DOCUMENT_READY = "document_ready", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", SOCKET_DISCONNECT = "disconnect", @@ -70,8 +71,15 @@ export const join = ( }); }; -export const initDocument = (roomId: string, template: string) => { +export const initDocument = (uid: string, roomId: string, template: string) => { collabSocket.emit(CollabEvents.INIT_DOCUMENT, roomId, template); + + return new Promise((resolve) => { + collabSocket.once(CollabEvents.UPDATE, (update) => { + applyUpdateV2(doc, new Uint8Array(update), uid); + resolve(); + }); + }); }; export const leave = (uid: string, roomId: string) => { From e8a917acc2edcb3f55518838b28a0d5181abdf1c Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:49:07 +0800 Subject: [PATCH 093/192] Submit button --- .../controllers/codeExecutionControllers.ts | 1 - .../CollabSessionControls/index.tsx | 22 ++--- frontend/src/contexts/MatchContext.tsx | 94 +++++++++++++++++-- frontend/src/utils/api.ts | 6 ++ 4 files changed, 98 insertions(+), 25 deletions(-) diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index 3baa5f3d4f..7ea3e053f2 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -95,7 +95,6 @@ export const executeCode = async (req: Request, res: Response) => { data: compilerData, }); } catch (err) { - console.log(err); res.status(500).json({ message: ERROR_FAILED_TO_EXECUTE_MESSAGE }); } }; diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 3f261e0077..13ed4bd049 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -3,11 +3,11 @@ import Stopwatch from "../Stopwatch"; import { useMatch } from "../../contexts/MatchContext"; import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; import { useEffect, useState } from "react"; -import { - extractHoursFromTime, - extractMinutesFromTime, - extractSecondsFromTime, -} from "../../utils/sessionTime"; +// import { +// extractHoursFromTime, +// extractMinutesFromTime, +// extractSecondsFromTime, +// } from "../../utils/sessionTime"; const CollabSessionControls: React.FC = () => { const [time, setTime] = useState(0); @@ -25,7 +25,7 @@ const CollabSessionControls: React.FC = () => { if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { handleEndSessionClick } = match; + const { handleSubmitSessionClick, handleEndSessionClick } = match; return ( @@ -37,15 +37,7 @@ const CollabSessionControls: React.FC = () => { }} variant="outlined" color="success" - onClick={() => { - console.log( - `Time taken: ${extractHoursFromTime( - time - )} hrs ${extractMinutesFromTime( - time - )} mins ${extractSecondsFromTime(time)} secs` - ); - }} // TODO: implement submit function with time taken pop-up + onClick={() => handleSubmitSessionClick(time)} > Submit diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 3bd84c5da1..2dbc2401fd 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-refresh/only-export-components */ -import { createContext, useContext, useEffect, useState } from "react"; +import React, { createContext, useContext, useEffect, useState } from "react"; import { matchSocket } from "../utils/matchSocket"; import { ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE, @@ -18,6 +18,18 @@ import useAppNavigate from "../components/UseAppNavigate"; import { UNSAFE_NavigationContext } from "react-router-dom"; import { Action, type History, type Transition } from "history"; +import { codeExecutionClient } from "../utils/api"; +import { useReducer } from "react"; +import { + createQnHistory, + updateQnHistoryById, +} from "../reducers/qnHistoryReducer"; +import qnReducer, { + getQuestionById, + initialState, +} from "../reducers/questionReducer"; +import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; + let matchUserId: string; let partnerUserId: string; @@ -85,6 +97,7 @@ type MatchContextType = { matchOfferTimeout: () => void; verifyMatchStatus: () => void; getMatchId: () => string | null; + handleSubmitSessionClick: (time: number) => void; handleEndSessionClick: () => void; handleRejectEndSession: () => void; handleConfirmEndSession: () => void; @@ -96,6 +109,11 @@ type MatchContextType = { isEndSessionModalOpen: boolean; questionId: string | null; qnHistoryId: string | null; + + questionTitle: string; + setQuestionTitle: React.Dispatch>; + code: string; + setCode: React.Dispatch>; }; const requestTimeoutDuration = 5000; @@ -287,12 +305,15 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const initMatchedListeners = () => { - matchSocket.on(MatchEvents.MATCH_SUCCESSFUL, (qnId: string, qnHistId: string) => { - setMatchPending(false); - setQuestionId(qnId); - setQnHistoryId(qnHistId); - appNavigate(MatchPaths.COLLAB); - }); + matchSocket.on( + MatchEvents.MATCH_SUCCESSFUL, + (qnId: string, qnHistId: string) => { + setMatchPending(false); + setQuestionId(qnId); + setQnHistoryId(qnHistId); + appNavigate(MatchPaths.COLLAB); + } + ); matchSocket.on(MatchEvents.MATCH_UNSUCCESSFUL, () => { toast.error(MATCH_UNSUCCESSFUL_MESSAGE); @@ -407,7 +428,12 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const acceptMatch = () => { - matchSocket.emit(MatchEvents.MATCH_ACCEPT_REQUEST, matchId, matchUserId, partnerUserId); + matchSocket.emit( + MatchEvents.MATCH_ACCEPT_REQUEST, + matchId, + matchUserId, + partnerUserId + ); }; const rematch = () => { @@ -507,9 +533,53 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { return matchId; }; + // eslint-disable-next-line no-unused-vars + const [state, dispatch] = useReducer(qnHistoryReducer, initialQHState); + const [questionTitle, setQuestionTitle] = useState(""); + const [code, setCode] = useState( + "a=input()\nb=input()\nc=input()\nd=input()\n\nprint(int(a)+int(b)+int(c)+int(d))" + ); + + const handleSubmitSessionClick = async (time: number) => { + try { + const res = await codeExecutionClient.post("/", { + questionId, + code: code, + language: matchCriteria?.language.toLowerCase(), + }); + + let isMatch = true; + for (let i = 0; i < res.data.data.length; i++) { + if (!res.data.data[i].isMatch) { + isMatch = false; + } + break; + } + + if (!isMatch) { + toast.error("Your code did not pass all the test cases."); + } else { + toast.success("You have successfully solved the question!"); + } + + updateQnHistoryById( + qnHistoryId as string, + { + submissionStatus: isMatch ? "Accepted" : "Rejected", + dateAttempted: new Date().toISOString(), + timeTaken: time, + code, + }, + dispatch + ); + } catch (err) { + toast.error(err.response?.data.message || err.message); + } + }; + const handleEndSessionClick = () => { setIsEndSessionModalOpen(true); - } + }; const handleRejectEndSession = () => { setIsEndSessionModalOpen(false); @@ -532,6 +602,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { matchOfferTimeout, verifyMatchStatus, getMatchId, + handleSubmitSessionClick, handleEndSessionClick, handleRejectEndSession, handleConfirmEndSession, @@ -543,6 +614,11 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { isEndSessionModalOpen, questionId, qnHistoryId, + + questionTitle, + setQuestionTitle, + code, + setCode, }} > {children} diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index 144e61990e..8f834d5a9e 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -2,6 +2,7 @@ import axios from "axios"; const usersUrl = "http://localhost:3001/api"; const questionsUrl = "http://localhost:3000/api/questions"; +const codeExecutionUrl = "http://localhost:3004/api/run"; const qnHistoriesUrl = "http://localhost:3006/api/qnhistories"; export const questionClient = axios.create({ @@ -14,6 +15,11 @@ export const userClient = axios.create({ withCredentials: true, }); +export const codeExecutionClient = axios.create({ + baseURL: codeExecutionUrl, + withCredentials: true, +}); + export const qnHistoryClient = axios.create({ baseURL: qnHistoriesUrl, withCredentials: true, From 78bfcdb9d0845dcd8d9c98ccc47e6c168918b1eb Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 5 Nov 2024 03:19:23 +0800 Subject: [PATCH 094/192] Create new redis instance for collab service --- README.md | 1 + backend/README.md | 4 ++ backend/code-execution-service/.env.sample | 5 ++- backend/code-execution-service/README.md | 10 ++++- backend/collab-service/.env.sample | 6 ++- backend/collab-service/README.md | 40 ++++++++++--------- backend/collab-service/src/config/redis.ts | 2 +- backend/communication-service/.env.sample | 3 +- backend/matching-service/.env.sample | 20 +++++----- backend/matching-service/README.md | 22 ++++++---- .../matching-service/src/config/rabbitmq.ts | 4 +- backend/qn-history-service/.env.sample | 10 +++-- backend/qn-history-service/README.md | 16 ++++++-- backend/question-service/.env.sample | 18 +++++---- backend/question-service/README.md | 24 +++++++++-- backend/user-service/.env.sample | 31 +++++++------- backend/user-service/README.md | 26 ++++++------ backend/user-service/src/config/redis.ts | 4 -- docker-compose-test.yml | 2 + docker-compose.yml | 35 +++++++++++----- 20 files changed, 179 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index b7f8659cc5..cc617440ea 100644 --- a/README.md +++ b/README.md @@ -31,5 +31,6 @@ docker-compose down - Matching Service: http://localhost:3002 - Collab Service: http://localhost:3003 - Code Execution Service: http://localhost:3004 +- Communication Service: http://localhost:3005 - Question History Service: http://localhost:3006 - Frontend: http://localhost:5173 diff --git a/backend/README.md b/backend/README.md index ea15cc12e4..0132d9f029 100644 --- a/backend/README.md +++ b/backend/README.md @@ -14,6 +14,10 @@ 3. Enter `host.docker.internal` as the Host. + 4. Enter the port used by the respective service: + - User Service: `6379` + - Collab Service: `6380` + 4. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. ## Setting-up cloud MongoDB (in production) diff --git a/backend/code-execution-service/.env.sample b/backend/code-execution-service/.env.sample index 561d77ee13..eeadbbcd3c 100644 --- a/backend/code-execution-service/.env.sample +++ b/backend/code-execution-service/.env.sample @@ -1,8 +1,9 @@ NODE_ENV=development SERVICE_PORT=3004 +# Origins for cors ORIGINS=http://localhost:5173,http://127.0.0.1:5173 -# One Compiler +# One Compiler configuration ONE_COMPILER_URL=https://onecompiler-apis.p.rapidapi.com/api/v1/run -ONE_COMPILER_KEY= \ No newline at end of file +ONE_COMPILER_KEY= diff --git a/backend/code-execution-service/README.md b/backend/code-execution-service/README.md index 64c4bd0e0e..447d154faa 100644 --- a/backend/code-execution-service/README.md +++ b/backend/code-execution-service/README.md @@ -6,9 +6,9 @@ 2. Sign up for a free OneCompiler API [here](https://rapidapi.com/onecompiler-onecompiler-default/api/onecompiler-apis). -3. Update `ONE_COMPILER_KEY` in `.env` with the the value of `x-rapidapi-key`. +3. Update `ONE_COMPILER_KEY` in the `.env` file with the value of `x-rapidapi-key`. -## Running Code Execution Service without Docker +## Running Code Execution Service Locally 1. Open Command Line/Terminal and navigate into the `code-execution-service` directory. @@ -16,6 +16,12 @@ 3. Run the command `npm start` to start the Code Execution Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. +## Running Code Execution Service with Docker + +1. Open the Command Line/Terminal. + +2. Run the command `docker compose run code-execution-service` to start up the Code Execution Service and its dependencies. + ## After running 1. To view Code Execution Service documentation, go to http://localhost:3004/docs. diff --git a/backend/collab-service/.env.sample b/backend/collab-service/.env.sample index 77915e9467..94020b37a8 100644 --- a/backend/collab-service/.env.sample +++ b/backend/collab-service/.env.sample @@ -1,9 +1,11 @@ NODE_ENV=development SERVICE_PORT=3003 +# Origins for cors ORIGINS=http://localhost:5173,http://127.0.0.1:5173 -REDIS_URI=redis://redis:6379 +# Redis configuration +REDIS_URI=redis://collab-service-redis:6379 -# Test +# Tests REDIS_URI_TEST=redis://test-redis:6379 diff --git a/backend/collab-service/README.md b/backend/collab-service/README.md index fd96790327..45cbd1def9 100644 --- a/backend/collab-service/README.md +++ b/backend/collab-service/README.md @@ -8,45 +8,49 @@ - `REDIS_URI` -## Running Collab Service Individually +## Running Collab Service Locally -1. Set up and run Redis using `docker compose run --rm --name redis -p 6379:6379 redis`. +1. Set up and run Redis using `docker compose run --rm --name collab-service-redis -p 6380:6379 collab-service-redis`. -2. Open Command Line/Terminal and navigate into the `collab-service` directory. +2. Comment out `REDIS_URI` in the `.env` file. -3. Run the command: `npm install`. This will install all the necessary dependencies. +3. Open Command Line/Terminal and navigate into the `collab-service` directory. -4. Run the command `npm start` to start the Collab Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. +4. Run the command `npm install`. This will install all the necessary dependencies. -## Running Collab Service Individually with Docker +5. Run the command `npm start` to start the Collab Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. -1. Open the command line/terminal. +## Running Collab Service with Docker -2. Run the command `docker compose run collab-service` to start up the collab service and its dependencies. +1. Open the Command Line/Terminal. + +2. Run the command `docker compose run collab-service` to start up the Collab Service and its dependencies. ## After running 1. Using applications like Postman, you can interact with the Collab Service on port 3003. If you wish to change this, please update the `.env` file. 2. Setting up Socket.IO connection on Postman: + - You should open 2 tabs on Postman to simulate 2 users in the Collab Service. - Select the `Socket.IO` option and set URL to `http://localhost:3003`. Click `Connect`. - ![image1.png](docs/image1.png) - + ![image1.png](docs/image1.png) + - Add the following events in the `Events` tab and listen to them. - ![image2.png](docs/image2.png) - + ![image2.png](docs/image2.png) + - To send a message, go to the `Message` tab and ensure that your message is being parsed as `JSON`. - ![image3.png](docs/image3.png) - + ![image3.png](docs/image3.png) + - In the `Event name` input, input the correct event name. Click on `Send` to send a message. - ![image4.png](docs/image4.png) + ![image4.png](docs/image4.png) + +## Events Available -## Events Available | Event Name | Description | Parameters | Response Event | -|----------------|-----------------------------------|-------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| -------------- | --------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **join** | Joins a collaboration room. | `roomId` (string): ID of the room. | **room_full:** Notify the user if the room is full (only 2 users allowed).
**connected:** Notify the user if successfully connected.
**new_user_connected:** Notify the other user if a new user joins the room. | | **change** | Sends updated code to other user. | `roomId` (string): ID of the room.
`code` (string): Updated code content. | **code_change:** Notify the other user with the updated code content. | | **leave** | Leaves the collaboration room. | `roomId` (string): ID of the room. | **partner_left:** Notify the other user when one leaves the room. | -| **disconnect** | Disconnects from the server. | None | **partner_disconnected:** Notify the other user when one is disconnected. | \ No newline at end of file +| **disconnect** | Disconnects from the server. | None | **partner_disconnected:** Notify the other user when one is disconnected. | diff --git a/backend/collab-service/src/config/redis.ts b/backend/collab-service/src/config/redis.ts index 6752ec0ab2..65b44112e9 100644 --- a/backend/collab-service/src/config/redis.ts +++ b/backend/collab-service/src/config/redis.ts @@ -6,7 +6,7 @@ dotenv.config(); const REDIS_URI = process.env.NODE_ENV === "test" ? process.env.REDIS_URI_TEST - : process.env.REDIS_URI || "redis://localhost:6379"; + : process.env.REDIS_URI || "redis://localhost:6380"; const client = createClient({ url: REDIS_URI }); diff --git a/backend/communication-service/.env.sample b/backend/communication-service/.env.sample index 07d3ce1596..e5f4ba279e 100644 --- a/backend/communication-service/.env.sample +++ b/backend/communication-service/.env.sample @@ -1,4 +1,5 @@ NODE_ENV=development SERVER_PORT=3005 -ORIGINS=http://localhost:5173,http://127.0.0.1:5173 \ No newline at end of file +# Origins for cors +ORIGINS=http://localhost:5173,http://127.0.0.1:5173 diff --git a/backend/matching-service/.env.sample b/backend/matching-service/.env.sample index 30292db838..83217a9e53 100644 --- a/backend/matching-service/.env.sample +++ b/backend/matching-service/.env.sample @@ -1,18 +1,16 @@ NODE_ENV=development SERVICE_PORT=3002 +# Origins for cors ORIGINS=http://localhost:5173,http://127.0.0.1:5173 - -## FOR RABBITMQ, comment out the variables under whichever use case (1) or (2) that is not applicable -# (1) RabbitMq for running matching service individually -RABBITMQ_ADDR=amqp://localhost:5672 #comment out if use case is (2) - -# (2) RabbitMq for running matching service with other services using docker compose -RABBITMQ_DEFAULT_USER=admin #comment out if use case is (1) -RABBITMQ_DEFAULT_PASS=password #comment out if use case is (1) -RABBITMQ_ADDR=amqp://admin:password@rabbitmq:5672 #comment out if use case is (1) - +# Other service APIs QUESTION_SERVICE_URL=http://question-service:3000/api/questions - QN_HISTORY_SERVICE_URL=http://qn-history-service:3006/api/qnhistories + +# RabbitMq configuration +RABBITMQ_DEFAULT_USER=admin +RABBITMQ_DEFAULT_PASS=password + +## Do not change anything below this line +RABBITMQ_ADDR=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672 diff --git a/backend/matching-service/README.md b/backend/matching-service/README.md index 323875d960..597f6f26a4 100644 --- a/backend/matching-service/README.md +++ b/backend/matching-service/README.md @@ -4,21 +4,27 @@ ## Setting-up Matching Service -1. In the `matching-service` directory, create a copy of the `.env.sample` file and name it `.env`. If you are looking to run matching service with the other services using docker-compose, comment out the variable `RABBITMQ_ADDR` under use case (1) in the .env file. Otherwise, if you are looking to run matching service individually, comment out the variables `RABBITMQ_DEFAULT_USER`, `RABBITMQ_DEFAULT_PASS` and `RABBITMQ_ADDR` under use case (2) in the .env file. +1. In the `matching-service` directory, create a copy of the `.env.sample` file and name it `.env`. -2. If you are running matching service together with other services using docker-compose, to set up credentials for RabbitMq, update the RabbitMq variables in the `.env` file. Update `RABBITMQ_DEFAULT_USER` and `RABBITMQ_DEFAULT_PASS` to what you want, then update `RABBITMQ_ADDR` to be `amqp://:@rabbitmq:5672`. You can access RabbitMq management user interface locally with the username in `RABBITMQ_DEFAULT_USER` and password in `RABBITMQ_DEFAULT_PASS` at http://localhost:15672. +2. You can access RabbitMq management user interface locally with `RABBITMQ_DEFAULT_USER` as the username and `RABBITMQ_DEFAULT_PASS` as the password at http://localhost:15672. You may update `RABBITMQ_DEFAULT_USER` and `RABBITMQ_DEFAULT_PASS` in the `.env` file to change your RabbitMq credentials if necessary. -3. If you are running matching service individually, you do not need to make any changes to `RABBITMQ_ADDR`. You can access RabbitMq management user interface locally with the username `guest` and password `guest` at http://localhost:15672. +## Running Matching Service Locally -## Running Matching Service Individually with Docker +1. Set up and run RabbitMq using `docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4.0-management`. -1. Set up and run RabbitMq locally on your computer with the command `docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4.0-management`. +2. Comment out `RABBITMQ_ADDR` in the `.env` file. You can access RabbitMq management user interface locally with the username `guest` and password `guest` at http://localhost:15672. -2. Open Command Line/Terminal and navigate into the `matching-service` directory. +3. Open Command Line/Terminal and navigate into the `matching-service` directory. -3. Run the command: `npm install`. This will install all the necessary dependencies. +4. Run the command: `npm install`. This will install all the necessary dependencies. -4. Run the command `npm start` to start the Matching Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. If you encounter connection errors, please wait for a few minutes before running `npm start` again as RabbitMq may take some time to start up. +5. Run the command `npm start` to start the Matching Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. If you encounter connection errors, please wait for a few minutes before running `npm start` again as RabbitMq may take some time to start up. + +## Running Matching Service with Docker + +1. Open the Command Line/Terminal. + +2. Run the command `docker compose run matching-service` to start up the Matching Service and its dependencies. ## After running diff --git a/backend/matching-service/src/config/rabbitmq.ts b/backend/matching-service/src/config/rabbitmq.ts index 9841e3808b..966e98cbd6 100644 --- a/backend/matching-service/src/config/rabbitmq.ts +++ b/backend/matching-service/src/config/rabbitmq.ts @@ -6,6 +6,8 @@ import { Complexities, Categories, Languages } from "../utils/constants"; dotenv.config(); +const RABBITMQ_ADDR = process.env.RABBITMQ_ADDR || "amqp://localhost:5672"; + let mrConnection: Connection; const queues: string[] = []; const pendingQueueRequests = new Map>(); @@ -35,7 +37,7 @@ const setUpQueue = async (queueName: string) => { export const connectToRabbitMq = async () => { try { initQueueNames(); - mrConnection = await amqplib.connect(`${process.env.RABBITMQ_ADDR}`); + mrConnection = await amqplib.connect(RABBITMQ_ADDR); for (const queue of queues) { await setUpQueue(queue); pendingQueueRequests.set(queue, new Map()); diff --git a/backend/qn-history-service/.env.sample b/backend/qn-history-service/.env.sample index 0583d0a3e4..097fb51765 100644 --- a/backend/qn-history-service/.env.sample +++ b/backend/qn-history-service/.env.sample @@ -1,14 +1,16 @@ NODE_ENV=development SERVICE_PORT=3006 +# Origins for cors ORIGINS=http://localhost:5173,http://127.0.0.1:5173 -# if using cloud MongoDB, replace with actual URI (run service separately) -MONGO_CLOUD_URI= - +# Tests MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ -# if using local MongoDB (run service with docker-compose) +# If using cloud MongoDB, replace with actual URI (run service separately) +MONGO_CLOUD_URI= + +# If using local MongoDB (run service with docker-compose) ## MongoDB credentials MONGO_INITDB_ROOT_USERNAME=root MONGO_INITDB_ROOT_PASSWORD=example diff --git a/backend/qn-history-service/README.md b/backend/qn-history-service/README.md index 2a15faaea3..ee6f1b1cf2 100644 --- a/backend/qn-history-service/README.md +++ b/backend/qn-history-service/README.md @@ -8,13 +8,17 @@ 2. To connect to your cloud MongoDB instead of your local MongoDB, set the `NODE_ENV` to `production` instead of `development`. -3. Update `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` to change your MongoDB credentials if necessary. +3. Update the following variable in the `.env` file: + + - `MONGO_CLOUD_URI` + + You can also update `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` to change your MongoDB credentials if necessary. 4. You can view the MongoDB collections locally using Mongo Express. To set up Mongo Express, update `ME_CONFIG_BASICAUTH_USERNAME` and `ME_CONFIG_BASICAUTH_PASSWORD`. The username and password will be the login credentials when you access Mongo Express at http://localhost:8083. -## Running Question History Service without Docker +## Running Question History Service Locally -> Make sure you have the cloud MongoDB URI in your .env file and set NODE_ENV to production already. +> Make sure you have the cloud MongoDB URI in your `.env` file and set `NODE_ENV` to `production` already. 1. Open Command Line/Terminal and navigate into the `qn-history-service` directory. @@ -22,6 +26,12 @@ 3. Run the command `npm start` to start the Question History Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. +## Running Question History Service with Docker + +1. Open the Command Line/Terminal. + +2. Run the command `docker compose run qn-history-service` to start up the Question History Service and its dependencies. + ## After running 1. To view Question History Service documentation, go to http://localhost:3006/docs. diff --git a/backend/question-service/.env.sample b/backend/question-service/.env.sample index adcfe2bbcf..160e56ed9a 100644 --- a/backend/question-service/.env.sample +++ b/backend/question-service/.env.sample @@ -1,21 +1,25 @@ NODE_ENV=development SERVICE_PORT=3000 -FIREBASE_PROJECT_ID= -FIREBASE_PRIVATE_KEY= -FIREBASE_CLIENT_EMAIL= -FIREBASE_STORAGE_BUCKET=>FIREBASE_STORAGE_BUCKET> - +# Origins for cors ORIGINS=http://localhost:5173,http://127.0.0.1:5173 +# Other service APIs USER_SERVICE_URL=http://user-service:3001/api +# Firebase configuration +FIREBASE_PROJECT_ID= +FIREBASE_PRIVATE_KEY= +FIREBASE_CLIENT_EMAIL= +FIREBASE_STORAGE_BUCKET= + +# Tests MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ -# if using cloud MongoDB, replace with actual URI (run service separately) +# If using cloud MongoDB, replace with actual URI (run service separately) MONGO_CLOUD_URI= -# if using local MongoDB (run service with docker-compose) +# If using local MongoDB (run service with docker-compose) ## MongoDB credentials MONGO_INITDB_ROOT_USERNAME=root MONGO_INITDB_ROOT_PASSWORD=example diff --git a/backend/question-service/README.md b/backend/question-service/README.md index 384013eedc..5b21ac964d 100644 --- a/backend/question-service/README.md +++ b/backend/question-service/README.md @@ -8,13 +8,25 @@ 2. To connect to your cloud MongoDB instead of your local MongoDB, set the `NODE_ENV` to `production` instead of `development`. -3. Update `FIREBASE_PROJECT_ID`, `FIREBASE_PRIVATE_KEY`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_STORAGE_BUCKET`, `MONGO_CLOUD_URI` with the env variables obtained from following the instructions in the backend README. Then update `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` to change your MongoDB credentials if necessary. +3. Update the following variables in the `.env` file: + + - `FIREBASE_PROJECT_ID` + + - `FIREBASE_PRIVATE_KEY` + + - `FIREBASE_CLIENT_EMAIL` + + - `FIREBASE_STORAGE_BUCKET` + + - `MONGO_CLOUD_URI` + + You can also update `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` to change your MongoDB credentials if necessary. 4. You can view the MongoDB collections locally using Mongo Express. To set up Mongo Express, update `ME_CONFIG_BASICAUTH_USERNAME` and `ME_CONFIG_BASICAUTH_PASSWORD`. The username and password will be the login credentials when you access Mongo Express at http://localhost:8081. -## Running Question Service without Docker +## Running Question Service Locally -> Make sure you have the cloud MongoDB URI in your .env file and set NODE_ENV to production already. +> Make sure you have the cloud MongoDB URI in your `.env` file and set `NODE_ENV` to `production` already. 1. Open Command Line/Terminal and navigate into the `question-service` directory. @@ -22,6 +34,12 @@ 3. Run the command `npm start` to start the Question Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. +## Running Question Service with Docker + +1. Open the Command Line/Terminal. + +2. Run the command `docker compose run question-service` to start up the Question Service and its dependencies. + ## Seeding questions into MongoDB 1. With Docker diff --git a/backend/user-service/.env.sample b/backend/user-service/.env.sample index b94597ae29..f609cb7e57 100644 --- a/backend/user-service/.env.sample +++ b/backend/user-service/.env.sample @@ -1,6 +1,9 @@ NODE_ENV=development SERVICE_PORT=3001 +# Origins for cors +ORIGINS=http://localhost:5173,http://127.0.0.1:5173 + # Secret for creating JWT signature JWT_SECRET= @@ -11,32 +14,28 @@ ADMIN_USERNAME=administrator ADMIN_EMAIL=admin@gmail.com ADMIN_PASSWORD=Admin@123 -# Firebase -FIREBASE_PROJECT_ID=FIREBASE_PROJECT_ID -FIREBASE_PRIVATE_KEY=FIREBASE_PRIVATE_KEY -FIREBASE_CLIENT_EMAIL=FIREBASE_CLIENT_EMAIL -FIREBASE_STORAGE_BUCKET=FIREBASE_STORAGE_BUCKET - -# Origins for cors -ORIGINS=http://localhost:5173,http://127.0.0.1:5173 +# Firebase configuration +FIREBASE_PROJECT_ID= +FIREBASE_PRIVATE_KEY= +FIREBASE_CLIENT_EMAIL= +FIREBASE_STORAGE_BUCKET= -# Mail service +# Mail service configuration SERVICE=gmail -USER=EMAIL_ADDRESS -PASS=PASSWORD +USER= +PASS= # Redis configuration -REDIS_URI=redis://redis:6379 # Uncomment if you're running the user service using docker compose -# REDIS_URI=redis://localhost:6379 # Uncomment if you're running the user service individually without docker +REDIS_URI=redis://user-service-redis:6379 -# Test +# Tests MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ REDIS_URI_TEST=redis://test-redis:6379 -# if using cloud MongoDB, replace with actual URI (run service separately) +# If using cloud MongoDB, replace with actual URI (run service separately) MONGO_CLOUD_URI= -# if using local MongoDB (run service with docker-compose) +# If using local MongoDB (run service with docker-compose) ## MongoDB credentials MONGO_INITDB_ROOT_USERNAME=root MONGO_INITDB_ROOT_PASSWORD=example diff --git a/backend/user-service/README.md b/backend/user-service/README.md index 0c04de5047..fb202181f7 100644 --- a/backend/user-service/README.md +++ b/backend/user-service/README.md @@ -10,7 +10,7 @@ 3. Update the following variables in the `.env` file: - - `MONGO_CLOUD_URI` + - `JWT_SECRET` - `FIREBASE_PROJECT_ID` @@ -20,8 +20,6 @@ - `FIREBASE_STORAGE_BUCKET` - - `JWT_SECRET` - - `SERVICE`: Email service to use to send account verification links, e.g. `gmail`. - `USER`: Email address that you will be using, e.g. `johndoe@gmail.com`. @@ -30,29 +28,33 @@ - `REDIS_URI` + - `MONGO_CLOUD_URI` + You can also update `MONGO_INITDB_ROOT_USERNAME`, `MONGO_INITDB_ROOT_PASSWORD` to change your MongoDB credentials if necessary. 4. You can view the MongoDB collections locally using Mongo Express. To set up Mongo Express, update `ME_CONFIG_BASICAUTH_USERNAME` and `ME_CONFIG_BASICAUTH_PASSWORD`. The username and password will be the login credentials when you access Mongo Express at http://localhost:8082. 5. A default admin account (`email: admin@gmail.com` and `password: Admin@123`) wil be created. If you wish to change the default credentials, update them in `.env`. Alternatively, you can also edit your credentials and user profile after you have created the default account. -## Running User Service Individually +## Running User Service Locally + +> Make sure you have the cloud MongoDB URI in your `.env` file and set `NODE_ENV` to `production` already. -> Make sure you have the cloud MongoDB URI in your .env file and set NODE_ENV to production already. +1. Set up and run Redis using `docker compose run --rm --name user-service-redis -p 6379:6379 user-service-redis`. -1. Set up and run Redis using `docker compose run --rm --name redis -p 6379:6379 redis`. +2. Comment out `REDIS_URI` in the `.env` file. -2. Open Command Line/Terminal and navigate into the `user-service` directory. +3. Open Command Line/Terminal and navigate into the `user-service` directory. -3. Run the command: `npm install`. This will install all the necessary dependencies. +4. Run the command: `npm install`. This will install all the necessary dependencies. -4. Run the command `npm start` to start the User Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. +5. Run the command `npm start` to start the User Service in production mode, or use `npm run dev` for development mode, which includes features like automatic server restart when you make code changes. -## Running User Service Individually with Docker +## Running User Service with Docker -1. Open the command line/terminal. +1. Open the Command Line/Terminal. -2. Run the command `docker compose run user-service` to start up the user service and its dependencies. +2. Run the command `docker compose run user-service` to start up the User Service and its dependencies. ## After running diff --git a/backend/user-service/src/config/redis.ts b/backend/user-service/src/config/redis.ts index 6f6e74a319..6752ec0ab2 100644 --- a/backend/user-service/src/config/redis.ts +++ b/backend/user-service/src/config/redis.ts @@ -15,8 +15,4 @@ export const connectRedis = async () => { client.on("error", (err) => console.log(`Error: ${err}`)); }; -// client.on("error", (err) => console.log(`Error: ${err}`)); - -// (async () => await client.connect())(); - export default client; diff --git a/docker-compose-test.yml b/docker-compose-test.yml index af72ced8ee..d6bbbf78a7 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -75,6 +75,8 @@ services: - NODE_ENV=test - SERVICE_PORT=3003 - REDIS_URI_TEST=redis://test-redis:6379 + depends_on: + - test-redis networks: - peerprep-network volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 07409c1f13..9af939d783 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - 3001:3001 depends_on: - user-service-mongo - - redis + - user-service-redis networks: - peerprep-network volumes: @@ -63,6 +63,8 @@ services: env_file: ./backend/collab-service/.env ports: - 3003:3003 + depends_on: + - collab-service-redis networks: - peerprep-network volumes: @@ -131,6 +133,8 @@ services: - question-service - matching-service - collab-service + - code-execution-service + - communication-service - qn-history-service networks: - peerprep-network @@ -224,15 +228,31 @@ services: timeout: 10s retries: 10 - redis: + user-service-redis: image: redis:8.0-M01 - container_name: redis + container_name: user-service-redis ports: - 6379:6379 networks: - peerprep-network volumes: - - redis-data:/data + - user-service-redis-data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + interval: 10s + timeout: 10s + retries: 10 + command: ["redis-server"] + + collab-service-redis: + image: redis:8.0-M01 + container_name: collab-service-redis + ports: + - 6380:6379 + networks: + - peerprep-network + volumes: + - collab-service-redis-data:/data healthcheck: test: ["CMD-SHELL", "redis-cli ping | grep PONG"] interval: 10s @@ -249,16 +269,13 @@ services: - peerprep-network volumes: - redis-insight-data:/data - depends_on: - redis: - condition: service_healthy - restart: true volumes: question-service-mongo-data: user-service-mongo-data: qn-history-service-mongo-data: - redis-data: + user-service-redis-data: + collab-service-redis-data: redis-insight-data: networks: From 0486444c7829d05ce419b38ab1f7dfe71b5c8e99 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 5 Nov 2024 03:36:49 +0800 Subject: [PATCH 095/192] Fix linting --- backend/collab-service/src/handlers/websocketHandler.ts | 2 +- frontend/src/components/CodeEditor/index.tsx | 2 ++ frontend/src/components/QuestionCodeTemplates/index.tsx | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 1c98b93305..1fa5b62a27 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -151,7 +151,7 @@ const getDocument = (roomId: string) => { let doc = collabSessions.get(roomId); if (!doc) { doc = new Doc(); - doc.on(CollabEvents.UPDATE, (_update) => { + doc.on(CollabEvents.UPDATE, () => { saveDocument(roomId, doc!); io.to(roomId).emit(CollabEvents.UPDATE, encodeStateAsUpdateV2(doc!)); }); diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 77f4910a00..d8d2db2331 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -56,6 +56,8 @@ const CodeEditor: React.FC = (props) => { setIsDocumentLoaded(true); }; loadTemplate(); + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isReadOnly, isEditorReady]); return ( diff --git a/frontend/src/components/QuestionCodeTemplates/index.tsx b/frontend/src/components/QuestionCodeTemplates/index.tsx index c1506b6118..93ebf23775 100644 --- a/frontend/src/components/QuestionCodeTemplates/index.tsx +++ b/frontend/src/components/QuestionCodeTemplates/index.tsx @@ -46,6 +46,7 @@ const QuestionCodeTemplates: React.FC = ({ })); }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleTabKeys = (event: any) => { const { value } = event.target; From a48b91477d15aa05b64dfe4007a686b65483a282 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:47:13 +0800 Subject: [PATCH 096/192] End session --- .../controllers/codeExecutionControllers.ts | 3 +- .../src/handlers/websocketHandler.ts | 1 + frontend/src/components/Chat/index.tsx | 2 +- frontend/src/components/CodeEditor/index.tsx | 4 +- .../CollabSessionControls/index.tsx | 2 +- frontend/src/contexts/MatchContext.tsx | 50 +++++++++++++++++-- frontend/src/utils/constants.ts | 8 +++ 7 files changed, 60 insertions(+), 10 deletions(-) diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index 3e463fc6da..3249d4990b 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -22,7 +22,6 @@ interface CompilerResult { export const executeCode = async (req: Request, res: Response) => { const { questionId, language, code } = req.body; - console.log(code); if (!language || !code || !questionId) { res.status(400).json({ @@ -95,7 +94,7 @@ export const executeCode = async (req: Request, res: Response) => { message: SUCCESS_MESSAGE, data: compilerData, }); - } catch (err) { + } catch { res.status(500).json({ message: ERROR_FAILED_TO_EXECUTE_MESSAGE }); } }; diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 1c98b93305..6346554a6d 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -151,6 +151,7 @@ const getDocument = (roomId: string) => { let doc = collabSessions.get(roomId); if (!doc) { doc = new Doc(); + // eslint-disable-next-line doc.on(CollabEvents.UPDATE, (_update) => { saveDocument(roomId, doc!); io.to(roomId).emit(CollabEvents.UPDATE, encodeStateAsUpdateV2(doc!)); diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 534a4ea52c..03fef29e0b 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -15,7 +15,7 @@ type Message = { createdTime: number; }; -enum CommunicationEvents { +export enum CommunicationEvents { // receive JOIN = "join", LEAVE = "leave", diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 87660dc596..99ac282ff0 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -70,7 +70,7 @@ const CodeEditor: React.FC = (props) => { setIsDocumentLoaded(true); }; loadTemplate(); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isReadOnly, isEditorReady]); return ( @@ -83,7 +83,7 @@ const CodeEditor: React.FC = (props) => { id="codeEditor" onChange={handleChange} extensions={[ - indentUnit.of(" "), + indentUnit.of("\t"), basicSetup(), languageSupport[language as keyof typeof languageSupport], ...(!isReadOnly && editorState diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 38c27bd422..085cb062bb 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -43,7 +43,7 @@ const CollabSessionControls: React.FC = () => { }} variant="outlined" color="error" - onClick={() => handleEndSessionClick()} + onClick={() =>{ handleEndSessionClick()}} > End Session diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 1187862f35..31ed92a368 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -11,6 +11,9 @@ import { MATCH_REQUEST_EXISTS_MESSAGE, MATCH_UNSUCCESSFUL_MESSAGE, USE_AUTH_ERROR_MESSAGE, + FAILED_TESTCASE_MESSAGE, + SUCCESS_TESTCASE_MESSAGE, + FAILED_TO_SUBMIT_CODE_MESSAGE, } from "../utils/constants"; import { useAuth } from "./AuthContext"; import { toast } from "react-toastify"; @@ -22,6 +25,9 @@ import { codeExecutionClient } from "../utils/api"; import { useReducer } from "react"; import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; +import { leave } from "../utils/collabSocket"; +import { CommunicationEvents } from "../components/Chat"; +import { communicationSocket } from "../utils/communicationSocket"; let matchUserId: string; let partnerUserId: string; @@ -39,6 +45,18 @@ type MatchCriteria = { timeout: number; }; +type CompilerResult = { + status: string; + exception: string | null; + stdout: string; + stderr: string | null; + executionTime: number; + stdin: string; + stout: string; + actualResult: string; + expectedResult: string; +}; + enum MatchEvents { // Send MATCH_REQUEST = "match_request", @@ -102,7 +120,9 @@ type MatchContextType = { isEndSessionModalOpen: boolean; questionId: string | null; qnHistoryId: string | null; + setCode: React.Dispatch>; + compilerResult: CompilerResult[]; }; const requestTimeoutDuration = 5000; @@ -139,6 +159,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { initialQHState ); const [code, setCode] = useState(""); + const [compilerResult, setCompilerResult] = useState([]); const navigator = useContext(UNSAFE_NavigationContext).navigator as History; @@ -537,6 +558,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { language: matchCriteria?.language.toLowerCase(), }); + setCompilerResult(res.data.data); + let isMatch = true; for (let i = 0; i < res.data.data.length; i++) { if (!res.data.data[i].isMatch) { @@ -545,10 +568,10 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { break; } - if (!isMatch) { - toast.error("Your code did not pass all the test cases."); + if (isMatch) { + toast.success(SUCCESS_TESTCASE_MESSAGE); } else { - toast.success("You have successfully solved the question!"); + toast.error(FAILED_TESTCASE_MESSAGE); } updateQnHistoryById( @@ -562,7 +585,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { qnHistoryDispatch ); } catch { - toast.error("Unable to submit code. Please try again later."); + toast.error(FAILED_TO_SUBMIT_CODE_MESSAGE); } }; @@ -576,6 +599,24 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const handleConfirmEndSession = () => { setIsEndSessionModalOpen(false); + + // Leave collaboration room + leave(matchUserId, getMatchId() as string); + leave(partnerUserId, getMatchId() as string); + + // Leave chat room + communicationSocket.emit( + CommunicationEvents.LEAVE, + getMatchId(), + matchUser?.username + ); + communicationSocket.emit( + CommunicationEvents.LEAVE, + getMatchId(), + partner?.username + ); + + // End match stopMatch(); }; @@ -604,6 +645,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { questionId, qnHistoryId, setCode, + compilerResult, }} > {children} diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index c570422c98..b68c120e2b 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -99,6 +99,14 @@ export const MATCH_CONNECTION_ERROR = export const QUESTION_DOES_NOT_EXIST_ERROR = "There are no questions with the specified complexity and category. Please try another combination."; +// Code execution +export const FAILED_TESTCASE_MESSAGE = + "Your code did not pass all the test cases."; +export const SUCCESS_TESTCASE_MESSAGE = + "You have successfully solved the question!"; +export const FAILED_TO_SUBMIT_CODE_MESSAGE = + "Unable to submit code. Please try again later."; + /* Alerts & Dialog Boxes */ // Questions export const ABORT_CREATE_OR_EDIT_QUESTION_CONFIRMATION_MESSAGE = From 1f284ec87f267d856b04b3d9efe71b173ace4a2d Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:53:58 +0800 Subject: [PATCH 097/192] Catch invalid file url in code exe --- .../controllers/codeExecutionControllers.ts | 8 ++++++++ .../src/utils/constants.ts | 2 ++ .../src/utils/testCasesApi.ts | 19 ++++++++++++------- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index 3249d4990b..4b2aca6cd4 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -7,6 +7,7 @@ import { ERROR_FAILED_TO_EXECUTE_MESSAGE, ERROR_NOT_SAME_LENGTH_MESSAGE, SUCCESS_MESSAGE, + ERROR_INVALID_TEST_CASES_MESSAGE, } from "../utils/constants"; import { questionService } from "../utils/questionApi"; import { testCasesApi } from "../utils/testCasesApi"; @@ -59,6 +60,13 @@ export const executeCode = async (req: Request, res: Response) => { return; } + if (stdinList.length === 0) { + res.status(400).json({ + message: ERROR_INVALID_TEST_CASES_MESSAGE, + }); + return; + } + // Execute code for each test case const compilerResponse = await oneCompilerApi(language, stdinList, code); diff --git a/backend/code-execution-service/src/utils/constants.ts b/backend/code-execution-service/src/utils/constants.ts index dedf32abef..1533c632ca 100644 --- a/backend/code-execution-service/src/utils/constants.ts +++ b/backend/code-execution-service/src/utils/constants.ts @@ -11,3 +11,5 @@ export const ERROR_NOT_SAME_LENGTH_MESSAGE = export const ERROR_FAILED_TO_EXECUTE_MESSAGE = "Failed to execute code"; export const SUCCESS_MESSAGE = "Code executed successfully"; + +export const ERROR_INVALID_TEST_CASES_MESSAGE = "Invalid test cases"; diff --git a/backend/code-execution-service/src/utils/testCasesApi.ts b/backend/code-execution-service/src/utils/testCasesApi.ts index 807728fa0e..7146fe7b11 100644 --- a/backend/code-execution-service/src/utils/testCasesApi.ts +++ b/backend/code-execution-service/src/utils/testCasesApi.ts @@ -4,12 +4,17 @@ export const testCasesApi = async ( inputFileUrl: string, outputFileUrl: string ) => { - const inputFileUrlResponse = await axios.get(inputFileUrl); - const outputFileUrlResponse = await axios.get(outputFileUrl); + try { + const inputFileUrlResponse = await axios.get(inputFileUrl); + const outputFileUrlResponse = await axios.get(outputFileUrl); - // Split the input and output files by double new line - return { - input: inputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"), - output: outputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"), - }; + // Split the input and output files by double new line + return { + input: inputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"), + output: outputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"), + }; + } catch { + console.log("Failed to fetch test cases"); + return { input: [], output: [] }; + } }; From 12f7dc4bc308fc47b2d1688e111868d9d0e8337e Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:28:54 +0800 Subject: [PATCH 098/192] Add collab context --- frontend/src/App.tsx | 94 +++++----- frontend/src/components/CodeEditor/index.tsx | 12 +- .../CollabSessionControls/index.tsx | 17 +- frontend/src/contexts/CollabContext.tsx | 160 ++++++++++++++++++ frontend/src/contexts/MatchContext.tsx | 107 +----------- frontend/src/pages/CollabSandbox/index.tsx | 16 +- frontend/src/utils/constants.ts | 2 + 7 files changed, 243 insertions(+), 165 deletions(-) create mode 100644 frontend/src/contexts/CollabContext.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 99cea9a241..bd13533118 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,7 @@ import ProfileContextProvider from "./contexts/ProfileContext"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import MatchProvider from "./contexts/MatchContext"; +import CollabProvider from "./contexts/CollabContext"; import CollabSandbox from "./pages/CollabSandbox"; import NoDirectAccessRoutes from "./components/NoDirectAccessRoutes"; import EmailVerification from "./pages/EmailVerification"; @@ -30,55 +31,60 @@ function App() { return ( - - }> - } /> - }> - } /> - - - } /> - } /> - }> - } /> - } /> + + + }> + } /> + }> + } /> - - - - - } - /> - } /> - }> - }> - } /> - } /> - } /> + + } /> + } /> + }> + } /> + } /> + - - }> - }> - } /> + + + + } + /> + } + /> + }> + }> + } /> + } /> + } /> + + }> + }> + } /> + + + } /> - } /> - - - } /> - } /> - } /> - - } /> - } /> + + } /> + } /> + } /> + + } /> + } /> + + } /> + } /> - } /> - } /> - - + + diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 99ac282ff0..5f9652d382 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -10,8 +10,8 @@ import { cursorExtension } from "../../utils/collabCursor"; import { yCollab } from "y-codemirror.next"; import { Text } from "yjs"; import { Awareness } from "y-protocols/awareness"; -import { useMatch } from "../../contexts/MatchContext"; -import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; +import { useCollab } from "../../contexts/CollabContext"; +import { USE_COLLAB_ERROR_MESSAGE } from "../../utils/constants"; interface CodeEditorProps { editorState?: { text: Text; awareness: Awareness }; @@ -40,12 +40,12 @@ const CodeEditor: React.FC = (props) => { isReadOnly = false, } = props; - const match = useMatch(); - if (!match) { - throw new Error(USE_MATCH_ERROR_MESSAGE); + const collab = useCollab(); + if (!collab) { + throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { setCode } = match; + const { setCode } = collab; const [isEditorReady, setIsEditorReady] = useState(false); const [isDocumentLoaded, setIsDocumentLoaded] = useState(false); diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 085cb062bb..a35599d7a8 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -1,7 +1,7 @@ import { Button, Stack } from "@mui/material"; import Stopwatch from "../Stopwatch"; -import { useMatch } from "../../contexts/MatchContext"; -import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; +import { useCollab } from "../../contexts/CollabContext"; +import { USE_COLLAB_ERROR_MESSAGE } from "../../utils/constants"; import { useEffect, useState } from "react"; const CollabSessionControls: React.FC = () => { @@ -16,11 +16,12 @@ const CollabSessionControls: React.FC = () => { return () => clearInterval(intervalId); }, [time]); - const match = useMatch(); - if (!match) { - throw new Error(USE_MATCH_ERROR_MESSAGE); + const collab = useCollab(); + if (!collab) { + throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { handleSubmitSessionClick, handleEndSessionClick } = match; + + const { handleSubmitSessionClick, handleEndSessionClick } = collab; return ( @@ -43,7 +44,9 @@ const CollabSessionControls: React.FC = () => { }} variant="outlined" color="error" - onClick={() =>{ handleEndSessionClick()}} + onClick={() => { + handleEndSessionClick(); + }} > End Session diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx new file mode 100644 index 0000000000..aa4a935447 --- /dev/null +++ b/frontend/src/contexts/CollabContext.tsx @@ -0,0 +1,160 @@ +/* eslint-disable react-refresh/only-export-components */ + +import React, { createContext, useContext, useState } from "react"; +import { + USE_MATCH_ERROR_MESSAGE, + FAILED_TESTCASE_MESSAGE, + SUCCESS_TESTCASE_MESSAGE, + FAILED_TO_SUBMIT_CODE_MESSAGE, +} from "../utils/constants"; +import { toast } from "react-toastify"; + +import { useMatch } from "./MatchContext"; +import { codeExecutionClient } from "../utils/api"; +import { useReducer } from "react"; +import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; +import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; +import { leave } from "../utils/collabSocket"; +import { CommunicationEvents } from "../components/Chat"; +import { communicationSocket } from "../utils/communicationSocket"; + +type CompilerResult = { + status: string; + exception: string | null; + stdout: string; + stderr: string | null; + executionTime: number; + stdin: string; + stout: string; + actualResult: string; + expectedResult: string; +}; + +type ContextContextType = { + handleSubmitSessionClick: (time: number) => void; + handleEndSessionClick: () => void; + handleRejectEndSession: () => void; + handleConfirmEndSession: () => void; + setCode: React.Dispatch>; + compilerResult: CompilerResult[]; +}; + +const CollabContext = createContext(null); + +const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { + const { children } = props; + + const match = useMatch(); + + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + + const { + matchUser, + partner, + matchCriteria, + getMatchId, + stopMatch, + questionId, + qnHistoryId, + setIsEndSessionModalOpen, + } = match; + + // eslint-disable-next-line + const [qnHistoryState, qnHistoryDispatch] = useReducer( + qnHistoryReducer, + initialQHState + ); + const [code, setCode] = useState(""); + const [compilerResult, setCompilerResult] = useState([]); + + const handleSubmitSessionClick = async (time: number) => { + try { + const res = await codeExecutionClient.post("/", { + questionId, + code, + language: matchCriteria?.language.toLowerCase(), + }); + + setCompilerResult(res.data.data); + + let isMatch = true; + for (let i = 0; i < res.data.data.length; i++) { + if (!res.data.data[i].isMatch) { + isMatch = false; + } + break; + } + + if (isMatch) { + toast.success(SUCCESS_TESTCASE_MESSAGE); + } else { + toast.error(FAILED_TESTCASE_MESSAGE); + } + + updateQnHistoryById( + qnHistoryId as string, + { + submissionStatus: isMatch ? "Accepted" : "Rejected", + dateAttempted: new Date().toISOString(), + timeTaken: time, + code, + }, + qnHistoryDispatch + ); + } catch { + toast.error(FAILED_TO_SUBMIT_CODE_MESSAGE); + } + }; + + const handleEndSessionClick = () => { + setIsEndSessionModalOpen(true); + }; + + const handleRejectEndSession = () => { + setIsEndSessionModalOpen(false); + }; + + const handleConfirmEndSession = () => { + setIsEndSessionModalOpen(false); + + // Leave collaboration room + leave(matchUser?.id as string, getMatchId() as string); + leave(partner?.id as string, getMatchId() as string); + + // Leave chat room + communicationSocket.emit( + CommunicationEvents.LEAVE, + getMatchId(), + matchUser?.username + ); + communicationSocket.emit( + CommunicationEvents.LEAVE, + getMatchId(), + partner?.username + ); + + // End match + stopMatch(); + }; + + return ( + + {children} + + ); +}; + +export const useCollab = () => useContext(CollabContext); + +export default CollabProvider; diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 31ed92a368..38281ddb0c 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -11,9 +11,6 @@ import { MATCH_REQUEST_EXISTS_MESSAGE, MATCH_UNSUCCESSFUL_MESSAGE, USE_AUTH_ERROR_MESSAGE, - FAILED_TESTCASE_MESSAGE, - SUCCESS_TESTCASE_MESSAGE, - FAILED_TO_SUBMIT_CODE_MESSAGE, } from "../utils/constants"; import { useAuth } from "./AuthContext"; import { toast } from "react-toastify"; @@ -21,13 +18,8 @@ import useAppNavigate from "../components/UseAppNavigate"; import { UNSAFE_NavigationContext } from "react-router-dom"; import { Action, type History, type Transition } from "history"; -import { codeExecutionClient } from "../utils/api"; import { useReducer } from "react"; -import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; -import { leave } from "../utils/collabSocket"; -import { CommunicationEvents } from "../components/Chat"; -import { communicationSocket } from "../utils/communicationSocket"; let matchUserId: string; let partnerUserId: string; @@ -45,18 +37,6 @@ type MatchCriteria = { timeout: number; }; -type CompilerResult = { - status: string; - exception: string | null; - stdout: string; - stderr: string | null; - executionTime: number; - stdin: string; - stout: string; - actualResult: string; - expectedResult: string; -}; - enum MatchEvents { // Send MATCH_REQUEST = "match_request", @@ -108,21 +88,15 @@ type MatchContextType = { matchOfferTimeout: () => void; verifyMatchStatus: () => void; getMatchId: () => string | null; - handleSubmitSessionClick: (time: number) => void; - handleEndSessionClick: () => void; - handleRejectEndSession: () => void; - handleConfirmEndSession: () => void; matchUser: MatchUser | null; matchCriteria: MatchCriteria | null; partner: MatchUser | null; matchPending: boolean; loading: boolean; isEndSessionModalOpen: boolean; + setIsEndSessionModalOpen: React.Dispatch>; questionId: string | null; qnHistoryId: string | null; - - setCode: React.Dispatch>; - compilerResult: CompilerResult[]; }; const requestTimeoutDuration = 5000; @@ -158,8 +132,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { qnHistoryReducer, initialQHState ); - const [code, setCode] = useState(""); - const [compilerResult, setCompilerResult] = useState([]); const navigator = useContext(UNSAFE_NavigationContext).navigator as History; @@ -550,76 +522,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { return matchId; }; - const handleSubmitSessionClick = async (time: number) => { - try { - const res = await codeExecutionClient.post("/", { - questionId, - code, - language: matchCriteria?.language.toLowerCase(), - }); - - setCompilerResult(res.data.data); - - let isMatch = true; - for (let i = 0; i < res.data.data.length; i++) { - if (!res.data.data[i].isMatch) { - isMatch = false; - } - break; - } - - if (isMatch) { - toast.success(SUCCESS_TESTCASE_MESSAGE); - } else { - toast.error(FAILED_TESTCASE_MESSAGE); - } - - updateQnHistoryById( - qnHistoryId as string, - { - submissionStatus: isMatch ? "Accepted" : "Rejected", - dateAttempted: new Date().toISOString(), - timeTaken: time, - code, - }, - qnHistoryDispatch - ); - } catch { - toast.error(FAILED_TO_SUBMIT_CODE_MESSAGE); - } - }; - - const handleEndSessionClick = () => { - setIsEndSessionModalOpen(true); - }; - - const handleRejectEndSession = () => { - setIsEndSessionModalOpen(false); - }; - - const handleConfirmEndSession = () => { - setIsEndSessionModalOpen(false); - - // Leave collaboration room - leave(matchUserId, getMatchId() as string); - leave(partnerUserId, getMatchId() as string); - - // Leave chat room - communicationSocket.emit( - CommunicationEvents.LEAVE, - getMatchId(), - matchUser?.username - ); - communicationSocket.emit( - CommunicationEvents.LEAVE, - getMatchId(), - partner?.username - ); - - // End match - stopMatch(); - }; - return ( = (props) => { matchOfferTimeout, verifyMatchStatus, getMatchId, - handleSubmitSessionClick, - handleEndSessionClick, - handleRejectEndSession, - handleConfirmEndSession, matchUser, matchCriteria, partner, matchPending, loading, isEndSessionModalOpen, + setIsEndSessionModalOpen, questionId, qnHistoryId, - setCode, - compilerResult, }} > {children} diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index feefbe4335..db14aa617d 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -12,8 +12,12 @@ import { Tabs, } from "@mui/material"; import classes from "./index.module.css"; +import { useCollab } from "../../contexts/CollabContext"; import { useMatch } from "../../contexts/MatchContext"; -import { USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; +import { + USE_COLLAB_ERROR_MESSAGE, + USE_MATCH_ERROR_MESSAGE, +} from "../../utils/constants"; import { useEffect, useReducer, useState } from "react"; import Loader from "../../components/Loader"; import ServerError from "../../components/ServerError"; @@ -67,8 +71,6 @@ const CollabSandbox: React.FC = () => { const { verifyMatchStatus, getMatchId, - handleRejectEndSession, - handleConfirmEndSession, matchUser, partner, matchCriteria, @@ -76,6 +78,14 @@ const CollabSandbox: React.FC = () => { isEndSessionModalOpen, questionId, } = match; + + const collab = useCollab(); + if (!collab) { + throw new Error(USE_COLLAB_ERROR_MESSAGE); + } + + const { handleRejectEndSession, handleConfirmEndSession } = collab; + const [state, dispatch] = useReducer(reducer, initialState); const { selectedQuestion } = state; const [selectedTab, setSelectedTab] = useState<"tests" | "chat">("tests"); diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index b68c120e2b..83e86ff0b2 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -9,6 +9,8 @@ export const USE_PROFILE_ERROR_MESSAGE = "useProfile() must be used within ProfileContextProvider"; export const USE_MATCH_ERROR_MESSAGE = "useMatch() must be used within MatchProvider"; +export const USE_COLLAB_ERROR_MESSAGE = + "useCollab() must be used within CollabProvider"; /* Name Validation */ export const NAME_REQUIRED_ERROR_MESSAGE = "Name is required"; From 8f663b3fe7d1305f18faf3787e1d04a06bfb9e03 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:43:12 +0800 Subject: [PATCH 099/192] Fix type --- frontend/src/contexts/CollabContext.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index aa4a935447..9cbc86f724 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -30,7 +30,7 @@ type CompilerResult = { expectedResult: string; }; -type ContextContextType = { +type CollabContextType = { handleSubmitSessionClick: (time: number) => void; handleEndSessionClick: () => void; handleRejectEndSession: () => void; @@ -39,7 +39,7 @@ type ContextContextType = { compilerResult: CompilerResult[]; }; -const CollabContext = createContext(null); +const CollabContext = createContext(null); const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const { children } = props; From 3503bcfaea451630ddca21358322587679198287 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Tue, 5 Nov 2024 18:10:55 +0800 Subject: [PATCH 100/192] Close communication socket when unmount --- .../src/handlers/websocketHandler.ts | 22 +--------- .../communication-service/src/utils/types.ts | 2 - frontend/src/components/Chat/index.tsx | 41 +++++++++---------- 3 files changed, 20 insertions(+), 45 deletions(-) diff --git a/backend/communication-service/src/handlers/websocketHandler.ts b/backend/communication-service/src/handlers/websocketHandler.ts index 79351ad925..c61fc27c6f 100644 --- a/backend/communication-service/src/handlers/websocketHandler.ts +++ b/backend/communication-service/src/handlers/websocketHandler.ts @@ -13,7 +13,6 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { const room = io.sockets.adapter.rooms.get(roomId); if (room?.has(socket.id)) { - // todo: fetch messages from cache and send to the user socket.emit(CommunicationEvents.ALREADY_JOINED); return; } @@ -33,24 +32,6 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { } ); - socket.on( - CommunicationEvents.LEAVE, - ({ roomId, username }: { roomId: string; username: string }) => { - if (!roomId) { - return; - } - - socket.leave(roomId); - const createdTime = Date.now(); - socket.to(roomId).emit(CommunicationEvents.USER_LEFT, { - from: BOT_NAME, - type: MessageTypes.BOT_GENERATED, - message: `${username} has left the chat`, - createdTime, - }); - } - ); - socket.on( CommunicationEvents.SEND_TEXT_MESSAGE, ({ @@ -71,14 +52,13 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { message, createdTime, }); - - // todo: store the message in a cache } ); socket.on(CommunicationEvents.DISCONNECT, () => { const { roomId } = socket.data; if (roomId) { + console.log("disconnected", roomId, socket.data.username); const createdTime = Date.now(); socket.to(roomId).emit(CommunicationEvents.DISCONNECTED, { from: BOT_NAME, diff --git a/backend/communication-service/src/utils/types.ts b/backend/communication-service/src/utils/types.ts index ec56a8548f..59c993b1f3 100644 --- a/backend/communication-service/src/utils/types.ts +++ b/backend/communication-service/src/utils/types.ts @@ -1,12 +1,10 @@ export enum CommunicationEvents { // receive JOIN = "join", - LEAVE = "leave", SEND_TEXT_MESSAGE = "send_text_message", DISCONNECT = "disconnect", // send - USER_LEFT = "user_left", USER_JOINED = "user_joined", ALREADY_JOINED = "already_joined", TEXT_MESSAGE_RECEIVED = "text_message_received", diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 45a20f1a2d..6d08111ba4 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -18,12 +18,10 @@ type Message = { enum CommunicationEvents { // receive JOIN = "join", - LEAVE = "leave", SEND_TEXT_MESSAGE = "send_text_message", DISCONNECT = "disconnect", // send - USER_LEFT = "user_left", USER_JOINED = "user_joined", ALREADY_JOINED = "already_joined", TEXT_MESSAGE_RECEIVED = "text_message_received", @@ -68,32 +66,31 @@ const Chat: React.FC = ({ isActive }) => { username: user?.username, }); // eslint-disable-next-line react-hooks/exhaustive-deps + + return () => { + console.log("closing socket..."); + communicationSocket.close(); + setMessages([]); // clear the earlier messages in dev mode + }; }, []); useEffect(() => { // initliase listerner for incoming messages - communicationSocket.on( - CommunicationEvents.USER_JOINED, - (message: Message) => { - setMessages((prevMessages) => [...prevMessages, message]); - } - ); - communicationSocket.on( - CommunicationEvents.TEXT_MESSAGE_RECEIVED, - (message: Message) => { - setMessages((prevMessages) => [...prevMessages, message]); - } - ); - communicationSocket.on( - CommunicationEvents.DISCONNECTED, - (message: Message) => { - setMessages((prevMessages) => [...prevMessages, message]); - } - ); + const listener = (message: Message) => { + setMessages((prevMessages) => [...prevMessages, message]); + }; + + communicationSocket.on(CommunicationEvents.USER_JOINED, listener); + communicationSocket.on(CommunicationEvents.TEXT_MESSAGE_RECEIVED, listener); + communicationSocket.on(CommunicationEvents.DISCONNECTED, listener); return () => { - communicationSocket.off(CommunicationEvents.USER_JOINED); - communicationSocket.off(CommunicationEvents.TEXT_MESSAGE_RECEIVED); + communicationSocket.off(CommunicationEvents.USER_JOINED, listener); + communicationSocket.off( + CommunicationEvents.TEXT_MESSAGE_RECEIVED, + listener + ); + communicationSocket.off(CommunicationEvents.DISCONNECTED, listener); }; }, []); From 6a056825040c1d6c1824037973aae11e09541831 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 5 Nov 2024 22:03:20 +0800 Subject: [PATCH 101/192] Fix loading code template bug and css --- .../src/handlers/websocketHandler.ts | 49 ++++++++++++------- frontend/src/components/CodeEditor/index.tsx | 9 +++- frontend/src/contexts/CollabContext.tsx | 6 +-- frontend/src/pages/CollabSandbox/index.tsx | 5 +- frontend/src/utils/collabSocket.ts | 11 +---- 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 1fa5b62a27..3ab030f59d 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -92,27 +92,24 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { } ); - socket.on(CollabEvents.LEAVE, (uid: string, roomId: string) => { - const connectionKey = `${uid}:${roomId}`; - if (!userConnections.has(connectionKey)) { - return; - } - - clearTimeout(userConnections.get(connectionKey)!); + socket.on( + CollabEvents.LEAVE, + (uid: string, roomId: string, isImmediate: boolean) => { + const connectionKey = `${uid}:${roomId}`; + if (isImmediate || !userConnections.has(connectionKey)) { + handleUserLeave(uid, roomId, socket); + return; + } - const connectionTimeout = setTimeout(() => { - userConnections.delete(connectionKey); - socket.leave(roomId); - socket.disconnect(); + clearTimeout(userConnections.get(connectionKey)!); - const room = io.sockets.adapter.rooms.get(roomId); - if (!room || room.size === 0) { - removeCollabSession(roomId); - } - }, CONNECTION_DELAY); + const connectionTimeout = setTimeout(() => { + handleUserLeave(uid, roomId, socket); + }, CONNECTION_DELAY); - userConnections.set(connectionKey, connectionTimeout); - }); + userConnections.set(connectionKey, connectionTimeout); + } + ); socket.on(CollabEvents.RECONNECT_REQUEST, async (roomId: string) => { // TODO: Handle recconnection @@ -168,3 +165,19 @@ const saveDocument = async (roomId: string, doc: Doc) => { EX: EXPIRY_TIME, }); }; + +const handleUserLeave = (uid: string, roomId: string, socket: Socket) => { + const connectionKey = `${uid}:${roomId}`; + if (userConnections.has(connectionKey)) { + clearTimeout(userConnections.get(connectionKey)!); + userConnections.delete(connectionKey); + } + + socket.leave(roomId); + socket.disconnect(); + + const room = io.sockets.adapter.rooms.get(roomId); + if (!room || room.size === 0) { + removeCollabSession(roomId); + } +}; diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 5f9652d382..469c2c927a 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -76,7 +76,7 @@ const CodeEditor: React.FC = (props) => { return ( = (props) => { EditorView.editable.of(!isReadOnly && isDocumentLoaded), EditorState.readOnly.of(isReadOnly || !isDocumentLoaded), ]} - value={isReadOnly ? template : template ? "Loading code template..." : ""} + value={isReadOnly ? template : undefined} + placeholder={ + !isReadOnly && !isDocumentLoaded + ? "Loading code template..." + : undefined + } /> ); }; diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 9cbc86f724..c5ff38c1e8 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -62,7 +62,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } = match; // eslint-disable-next-line - const [qnHistoryState, qnHistoryDispatch] = useReducer( + const [_qnHistoryState, qnHistoryDispatch] = useReducer( qnHistoryReducer, initialQHState ); @@ -120,8 +120,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setIsEndSessionModalOpen(false); // Leave collaboration room - leave(matchUser?.id as string, getMatchId() as string); - leave(partner?.id as string, getMatchId() as string); + leave(matchUser?.id as string, getMatchId() as string, true); + leave(partner?.id as string, getMatchId() as string, true); // Leave chat room communicationSocket.emit( diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index db14aa617d..3c71d5900c 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -232,7 +232,8 @@ const CollabSandbox: React.FC = () => { sx={(theme) => ({ flex: 1, width: "100%", - maxHeight: "50vh", + minHeight: "44vh", + maxHeight: "44vh", paddingTop: theme.spacing(2), paddingBottom: theme.spacing(2), })} @@ -257,7 +258,7 @@ const CollabSandbox: React.FC = () => { { }); }; -export const leave = (uid: string, roomId: string) => { +export const leave = (uid: string, roomId: string, isImmediate?: boolean) => { collabSocket.removeAllListeners(); collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_SUCCESS); collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_FAILED); - collabSocket.emit(CollabEvents.LEAVE, uid, roomId); + collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isImmediate); doc.destroy(); }; @@ -135,10 +135,3 @@ const initConnectionStatusListeners = (roomId: string) => { }); } }; - -export const getDocumentContent = () => { - if (!doc.isDestroyed) { - return text.toString(); - } - return ""; -}; From bc7b1b0c70a83fac54ff26cb037921325600c817 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 5 Nov 2024 22:51:42 +0800 Subject: [PATCH 102/192] Redirect to home if error joining collab room --- frontend/src/pages/CollabSandbox/index.tsx | 54 ++++++++-------------- frontend/src/utils/constants.ts | 4 ++ 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 3c71d5900c..55f14d6c3d 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -15,12 +15,12 @@ import classes from "./index.module.css"; import { useCollab } from "../../contexts/CollabContext"; import { useMatch } from "../../contexts/MatchContext"; import { + COLLAB_CONNECTION_ERROR, USE_COLLAB_ERROR_MESSAGE, USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; import { useEffect, useReducer, useState } from "react"; import Loader from "../../components/Loader"; -import ServerError from "../../components/ServerError"; import reducer, { getQuestionById, initialState, @@ -32,6 +32,7 @@ import TabPanel from "../../components/TabPanel"; import TestCase from "../../components/TestCase"; import CodeEditor from "../../components/CodeEditor"; import { CollabSessionData, join, leave } from "../../utils/collabSocket"; +import { toast } from "react-toastify"; // hardcode for now... @@ -58,10 +59,10 @@ const testcases: TestCase[] = [ ]; const CollabSandbox: React.FC = () => { - const [showErrorScreen, setShowErrorScreen] = useState(false); const [editorState, setEditorState] = useState( null ); + const [isConnecting, setIsConnecting] = useState(true); const match = useMatch(); if (!match) { @@ -92,10 +93,6 @@ const CollabSandbox: React.FC = () => { const [selectedTestcase, setSelectedTestcase] = useState(0); useEffect(() => { - if (!partner) { - return; - } - verifyMatchStatus(); if (!questionId) { @@ -114,10 +111,12 @@ const CollabSandbox: React.FC = () => { if (editorState.ready) { setEditorState(editorState); } else { - setShowErrorScreen(true); + toast.error(COLLAB_CONNECTION_ERROR); + setIsConnecting(false); } } catch (error) { - console.error("Error connecting to collab session: ", error); + toast.error(COLLAB_CONNECTION_ERROR); + setIsConnecting(false); } }; @@ -128,37 +127,20 @@ const CollabSandbox: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - let timeout: number | undefined; - - if (!selectedQuestion) { - timeout = setTimeout(() => { - setShowErrorScreen(true); - }, 2000); - } else { - setShowErrorScreen(false); - } - - return () => clearTimeout(timeout); - }, [selectedQuestion]); - if (loading) { return ; } - if (!matchUser || !partner || !matchCriteria || !getMatchId()) { + if ( + !matchUser || + !partner || + !matchCriteria || + !getMatchId() || + !isConnecting + ) { return ; } - if (showErrorScreen) { - return ( - - ); - } - if (!selectedQuestion || !editorState) { return ; } @@ -234,8 +216,7 @@ const CollabSandbox: React.FC = () => { width: "100%", minHeight: "44vh", maxHeight: "44vh", - paddingTop: theme.spacing(2), - paddingBottom: theme.spacing(2), + paddingTop: theme.spacing(1), })} > { /> ({ flex: 1, maxHeight: "44vh", display: "flex", flexDirection: "column", - }} + paddingTop: theme.spacing(1), + })} > Date: Tue, 5 Nov 2024 22:55:47 +0800 Subject: [PATCH 103/192] Add seed qns, fix minor bug in code exe --- .../controllers/codeExecutionControllers.ts | 15 ++++++--- backend/question-service/src/scripts/seed.ts | 33 ++++++++++++------- .../longestPalindromicSubstringInput.txt | 3 ++ .../longestPalindromicSubstringOutput.txt | 3 ++ frontend/src/contexts/CollabContext.tsx | 2 +- 5 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 backend/question-service/src/scripts/testcases/longestPalindromicSubstringInput.txt create mode 100644 backend/question-service/src/scripts/testcases/longestPalindromicSubstringOutput.txt diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index 4b2aca6cd4..a2ac7f760e 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -79,11 +79,16 @@ export const executeCode = async (req: Request, res: Response) => { stdout = ""; } - // Extract the last line as the result value - // and the rest as stdout - const lines = stdout.trim().split("\n"); - const resultValue = lines.pop() || ""; - stdout = lines.join("\n"); + let resultValue = ""; + if (restofResult.stderr) { + resultValue = ""; + stdout = stdout.trim(); + } else { + // Extract the last line as the result value and the rest as stdout only if there is no error + const lines = stdout.trim().split("\n"); + resultValue = lines.pop() || ""; + stdout = lines.join("\n"); + } return { ...restofResult, diff --git a/backend/question-service/src/scripts/seed.ts b/backend/question-service/src/scripts/seed.ts index 44fe8bc81f..34b42e3386 100644 --- a/backend/question-service/src/scripts/seed.ts +++ b/backend/question-service/src/scripts/seed.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ + import { exit } from "process"; import connectDB from "../config/db"; import Question from "../models/Question"; @@ -38,7 +40,7 @@ export async function seedQuestions() { { title: "Two Sum", description: - "Given an array of integers `nums` and an integer `target`, return indices of the two numbers in a list such that they add up to `target`. You may assume that each input would have **exactly one solution**, and you may not use the same element twice. You can return the answer in any order.", + "Given an array of integers `nums` (line 1) and an integer `target` (line 2), return indices of the two numbers in a list such that they add up to `target`. You may assume that each input would have **exactly one solution**, and you may not use the same element twice. The list should be returned in sorted order.", complexity: "Easy", category: ["Arrays"], testcaseInputFileUrl: "./src/scripts/testcases/twoSumInput.txt", @@ -67,16 +69,7 @@ export async function seedQuestions() { "Each test case consists of two lines:\n" + "- The first line contains the elements of `nums1`, a sorted array of integers.\n" + "- The second line contains the elements of `nums2`, another sorted array of integers.\n\n" + - "Test cases are separated by a double newline. For example, an input file with two test cases could look like:\n" + - "```\n" + - "1 3\n2\n\n1 2\n3 4\n" + - "```\n\n" + - "### Output\n" + - "For each test case, output a single line containing the median of the two sorted arrays. Results should be separated by a double newline.\n\n" + - "The corresponding output file for the example above would be:\n" + - "```\n" + - "2.0\n\n2.5\n" + - "```\n\n" + + "For each test case, output a single line containing the median of the two sorted arrays.\n\n" + "### Explanation\n" + "- **Test Case 1**: `nums1 = [1, 3]` and `nums2 = [2]` have a combined sorted array `[1, 2, 3]`, with median `2.0`.\n" + "- **Test Case 2**: `nums1 = [1, 2]` and `nums2 = [3, 4]` have a combined sorted array `[1, 2, 3, 4]`, with median `2.5`.", @@ -91,6 +84,24 @@ export async function seedQuestions() { javaTemplate: `import java.util.*;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n List nums1 = new ArrayList<>();\n List nums2 = new ArrayList<>();\n boolean isNums2 = false;\n while (scanner.hasNextLine()) {\n String line = scanner.nextLine().trim();\n if (line.isEmpty()) {\n isNums2 = !isNums2;\n continue;\n }\n List nums = isNums2 ? nums2 : nums1;\n for (String num : line.split(" ")) {\n nums.add(Integer.parseInt(num));\n }\n }\n System.out.println(solution(nums1.stream().mapToInt(i -> i).toArray(), nums2.stream().mapToInt(i -> i).toArray()));\n }\n\n // Write your code here\n public static double solution(int[] nums1, int[] nums2) {\n // Implement your solution here\n return 0.0;\n }\n}`, cTemplate: `#include \n#include \n\n// Function to implement\ndouble solution(int* nums1, int nums1Size, int* nums2, int nums2Size) {\n // Implement your solution here\n return 0.0;\n}\n\n// Please do not modify the main function\nint main() {\n int nums1[100], nums2[100], n1 = 0, n2 = 0;\n char line[1000];\n while (fgets(line, sizeof(line), stdin)) {\n if (line[0] == '\\n') break;\n char* token = strtok(line, \" \");\n while (token) {\n nums1[n1++] = atoi(token);\n token = strtok(NULL, \" \");\n }\n }\n while (fgets(line, sizeof(line), stdin)) {\n if (line[0] == '\\n') break;\n char* token = strtok(line, \" \");\n while (token) {\n nums2[n2++] = atoi(token);\n token = strtok(NULL, \" \");\n }\n }\n printf(\"%.1f\\n\", solution(nums1, n1, nums2, n2));\n return 0;\n}`, }, + { + title: "Longest Palindromic Substring", + description: + "Given a string `s`, return the **longest palindromic substring** in `s`.\n\n" + + "For each test case, output a single line containing the longest palindromic substring in `s`.\n\n" + + "### Explanation\n" + + "- **Test Case 1**: For input `babad`, one of the longest palindromic substrings is `bab`.\n" + + "- **Test Case 2**: For input `cbbd`, the longest palindromic substring is `bb`.", + complexity: "Medium", + category: ["Strings", "Dynamic Programming"], + testcaseInputFileUrl: + "./src/scripts/testcases/longestPalindromicSubstringInput.txt", + testcaseOutputFileUrl: + "./src/scripts/testcases/longestPalindromicSubstringOutput.txt", + pythonTemplate: `# Please do not modify the main function\ndef main():\n\ts = input().strip()\n\tprint(solution(s))\n\n\n# Write your code here\ndef solution(s):\n\t# Implement your solution here\n\treturn ""\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `import java.util.Scanner;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n String s = scanner.nextLine().trim();\n System.out.println(solution(s));\n }\n\n // Write your code here\n public static String solution(String s) {\n // Implement your solution here\n return "";\n }\n}`, + cTemplate: `#include \n#include \n\n// Function to implement\nconst char* solution(const char* s) {\n // Implement your solution here\n return "";\n}\n\n// Please do not modify the main function\nint main() {\n char s[1000];\n fgets(s, sizeof(s), stdin);\n s[strcspn(s, "\\n")] = 0; // Remove newline\n printf("%s\\n", solution(s));\n return 0;\n}`, + }, ]; try { diff --git a/backend/question-service/src/scripts/testcases/longestPalindromicSubstringInput.txt b/backend/question-service/src/scripts/testcases/longestPalindromicSubstringInput.txt new file mode 100644 index 0000000000..666f5b9156 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/longestPalindromicSubstringInput.txt @@ -0,0 +1,3 @@ +babad + +cbbd \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/longestPalindromicSubstringOutput.txt b/backend/question-service/src/scripts/testcases/longestPalindromicSubstringOutput.txt new file mode 100644 index 0000000000..05f241cef7 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/longestPalindromicSubstringOutput.txt @@ -0,0 +1,3 @@ +bab + +bb \ No newline at end of file diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 9cbc86f724..44f8ed2cd6 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -83,8 +83,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { for (let i = 0; i < res.data.data.length; i++) { if (!res.data.data[i].isMatch) { isMatch = false; + break; } - break; } if (isMatch) { From ffd87c84e68e4f185b4aeeb58c65f71990206481 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:18:27 +0800 Subject: [PATCH 104/192] More qns --- backend/question-service/src/scripts/seed.ts | 36 ++++++++++++++++--- .../src/scripts/testcases/twoSum.py | 21 +++++++++++ .../src/scripts/testcases/zigzag.py | 31 ++++++++++++++++ .../src/scripts/testcases/zigzagInput.txt | 5 +++ .../src/scripts/testcases/zigzagOutput.txt | 3 ++ 5 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 backend/question-service/src/scripts/testcases/twoSum.py create mode 100644 backend/question-service/src/scripts/testcases/zigzag.py create mode 100644 backend/question-service/src/scripts/testcases/zigzagInput.txt create mode 100644 backend/question-service/src/scripts/testcases/zigzagOutput.txt diff --git a/backend/question-service/src/scripts/seed.ts b/backend/question-service/src/scripts/seed.ts index 34b42e3386..427003d35e 100644 --- a/backend/question-service/src/scripts/seed.ts +++ b/backend/question-service/src/scripts/seed.ts @@ -40,14 +40,14 @@ export async function seedQuestions() { { title: "Two Sum", description: - "Given an array of integers `nums` (line 1) and an integer `target` (line 2), return indices of the two numbers in a list such that they add up to `target`. You may assume that each input would have **exactly one solution**, and you may not use the same element twice. The list should be returned in sorted order.", + "Given an array of integers `nums` and an integer `target`, find the indices of the two numbers in `nums` that add up to `target`. You may assume that each input has **exactly one solution**, and you cannot use the same element twice. Return the indices as a list sorted in ascending order.", complexity: "Easy", category: ["Arrays"], testcaseInputFileUrl: "./src/scripts/testcases/twoSumInput.txt", testcaseOutputFileUrl: "./src/scripts/testcases/twoSumOutput.txt", - pythonTemplate: `# Please do not modify the main function\ndef main():\n\tprint(" ".join(solution()))\n\n\n# Write your code here\ndef solution():\n\treturn []\n\n\nif __name__ == "__main__":\n\tmain()\n`, - javaTemplate: `public class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n System.out.println(String.join(" ", solution()));\n }\n\n // Write your code here\n public static String[] solution() {\n return new String[]{};\n }\n}`, - cTemplate: `#include \n\n// Function to implement\nconst char** solution() {\n static const char* result[] = {NULL}; // Placeholder\n return result;\n}\n\n// Please do not modify the main function\nint main() {\n const char** result = solution();\n for (int i = 0; result[i] != NULL; i++) {\n printf("%s ", result[i]);\n }\n printf("\\n");\n return 0;\n}`, + pythonTemplate: `# Please do not modify the main function\ndef main():\n\tnums = list(map(int, input().split()))\n\ttarget = int(input().strip())\n\tprint(" ".join(map(str, solution(nums, target))))\n\n\n# Write your code here\ndef solution(nums, target):\n\t# Implement your solution here\n\treturn []\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `import java.util.Scanner;\nimport java.util.Arrays;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n int n = scanner.nextInt();\n int[] nums = new int[n];\n for (int i = 0; i < n; i++) {\n nums[i] = scanner.nextInt();\n }\n int target = scanner.nextInt();\n System.out.println(Arrays.toString(solution(nums, target)));\n }\n\n // Write your code here\n public static int[] solution(int[] nums, int target) {\n // Implement your solution here\n return new int[]{};\n }\n}`, + cTemplate: `#include \n#include \n\n// Function to implement\nint* solution(int* nums, int numsSize, int target, int* returnSize) {\n *returnSize = 2; // Adjust as needed\n int* result = (int*)malloc(2 * sizeof(int));\n // Implement your solution here\n result[0] = -1; // Placeholder\n result[1] = -1; // Placeholder\n return result;\n}\n\n// Please do not modify the main function\nint main() {\n int n, target;\n scanf("%d", &n);\n int nums[n];\n for (int i = 0; i < n; i++) {\n scanf("%d", &nums[i]);\n }\n scanf("%d", &target);\n int returnSize;\n int* result = solution(nums, n, target, &returnSize);\n for (int i = 0; i < returnSize; i++) {\n printf("%d ", result[i]);\n }\n printf("\\n");\n free(result);\n return 0;\n}`, }, { title: "Longest Substring Without Repeating Characters", @@ -102,6 +102,34 @@ export async function seedQuestions() { javaTemplate: `import java.util.Scanner;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n String s = scanner.nextLine().trim();\n System.out.println(solution(s));\n }\n\n // Write your code here\n public static String solution(String s) {\n // Implement your solution here\n return "";\n }\n}`, cTemplate: `#include \n#include \n\n// Function to implement\nconst char* solution(const char* s) {\n // Implement your solution here\n return "";\n}\n\n// Please do not modify the main function\nint main() {\n char s[1000];\n fgets(s, sizeof(s), stdin);\n s[strcspn(s, "\\n")] = 0; // Remove newline\n printf("%s\\n", solution(s));\n return 0;\n}`, }, + { + title: "ZigZag Conversion", + description: + "The string `PAYPALISHIRING` is written in a zigzag pattern on a given number of rows like this:\n\n" + + "```\n" + + "P A H N\n" + + "A P L S I I G\n" + + "Y I R\n" + + "```\n\n" + + "And then read line by line: `PAHNAPLSIIGYIR`.\n\n" + + "Write the code that will take a string and make this conversion given a number of rows.\n\n" + + "### Input\n" + + "Each test case consists of two lines:\n" + + "- The first line contains the string `s`.\n" + + "- The second line contains an integer `numRows`, representing the number of rows for the zigzag pattern.\n\n" + + "### Output\n" + + "For each test case, output the zigzag converted string as a single line.\n\n" + + "### Explanation\n" + + "- **Test Case 1**: `PAYPALISHIRING` with `numRows = 3` converts to `PAHNAPLSIIGYIR`.\n" + + "- **Test Case 2**: `PAYPALISHIRING` with `numRows = 4` converts to `HLOEL`.", + complexity: "Medium", + category: ["Strings"], + testcaseInputFileUrl: "./src/scripts/testcases/zigzagInput.txt", + testcaseOutputFileUrl: "./src/scripts/testcases/zigzagOutput.txt", + pythonTemplate: `# Please do not modify the main function\ndef main():\n\timport sys\n\tinput = sys.stdin.read().strip().split("\\n\\n")\n\tfor case in input:\n\t\tlines = case.split("\\n")\n\t\ts = lines[0]\n\t\tnumRows = int(lines[1])\n\t\tprint(solution(s, numRows))\n\n\n# Write your code here\ndef solution(s, numRows):\n\t# Implement your solution here\n\treturn ""\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `import java.util.*;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n String s = scanner.nextLine().trim();\n int numRows = Integer.parseInt(scanner.nextLine().trim());\n System.out.println(solution(s, numRows));\n }\n\n // Write your code here\n public static String solution(String s, int numRows) {\n // Implement your solution here\n return "";\n }\n}`, + cTemplate: `#include \n#include \n\n// Function to implement\nconst char* solution(const char* s, int numRows) {\n // Implement your solution here\n return "";\n}\n\n// Please do not modify the main function\nint main() {\n char s[1000];\n int numRows;\n fgets(s, sizeof(s), stdin);\n s[strcspn(s, "\\n")] = 0; // Remove newline\n scanf("%d", &numRows);\n printf("%s\\n", solution(s, numRows));\n return 0;\n}`, + }, ]; try { diff --git a/backend/question-service/src/scripts/testcases/twoSum.py b/backend/question-service/src/scripts/testcases/twoSum.py new file mode 100644 index 0000000000..ea7d80af36 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/twoSum.py @@ -0,0 +1,21 @@ +# Please do not modify the main function +def main(): + nums = list(map(int, input().split())) + target = int(input().strip()) + print(" ".join(map(str, solution(nums, target)))) + + +# Write your code here +def solution(nums, target): + # Implement your solution here + num_map = {} + for i, num in enumerate(nums): + complement = target - num + if complement in num_map: + return sorted([num_map[complement], i]) + num_map[num] = i + return [] + + +if __name__ == "__main__": + main() diff --git a/backend/question-service/src/scripts/testcases/zigzag.py b/backend/question-service/src/scripts/testcases/zigzag.py new file mode 100644 index 0000000000..db69a9453a --- /dev/null +++ b/backend/question-service/src/scripts/testcases/zigzag.py @@ -0,0 +1,31 @@ +# Please do not modify the main function +def main(): + import sys + + input = sys.stdin.read().strip().split("\n\n") + for case in input: + lines = case.split("\n") + s = lines[0] + numRows = int(lines[1]) + print(solution(s, numRows)) + + +# Write your code here +def solution(s, numRows): + if numRows == 1 or numRows >= len(s): + return s + + rows = [""] * numRows + current_row = 0 + going_down = False + + for char in s: + rows[current_row] += char + if current_row == 0 or current_row == numRows - 1: + going_down = not going_down + current_row += 1 if going_down else -1 + return "".join(rows) + + +if __name__ == "__main__": + main() diff --git a/backend/question-service/src/scripts/testcases/zigzagInput.txt b/backend/question-service/src/scripts/testcases/zigzagInput.txt new file mode 100644 index 0000000000..fcead7641a --- /dev/null +++ b/backend/question-service/src/scripts/testcases/zigzagInput.txt @@ -0,0 +1,5 @@ +PAYPALISHIRING +3 + +PAYPALISHIRING +4 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/zigzagOutput.txt b/backend/question-service/src/scripts/testcases/zigzagOutput.txt new file mode 100644 index 0000000000..0859fc0fb0 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/zigzagOutput.txt @@ -0,0 +1,3 @@ +PAHNAPLSIIGYIR + +PINALSIGYAHRPI \ No newline at end of file From 3079541adc0df47b63b20f15c6f9c058db57089c Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:37:18 +0800 Subject: [PATCH 105/192] More questions and answers --- .../question-service/src/scripts/README.md | 6 ++++ backend/question-service/src/scripts/seed.ts | 21 ++++++++++++ .../testcases/longestPalindromicSubstring.py | 34 +++++++++++++++++++ .../src/scripts/testcases/longestSubstring.py | 26 ++++++++++++++ .../scripts/testcases/medianTwoSortedArray.py | 27 +++++++++++++++ .../src/scripts/testcases/reverseInteger.py | 27 +++++++++++++++ .../scripts/testcases/reverseIntegerInput.txt | 5 +++ .../testcases/reverseIntegerOutput.txt | 5 +++ 8 files changed, 151 insertions(+) create mode 100644 backend/question-service/src/scripts/README.md create mode 100644 backend/question-service/src/scripts/testcases/longestPalindromicSubstring.py create mode 100644 backend/question-service/src/scripts/testcases/longestSubstring.py create mode 100644 backend/question-service/src/scripts/testcases/medianTwoSortedArray.py create mode 100644 backend/question-service/src/scripts/testcases/reverseInteger.py create mode 100644 backend/question-service/src/scripts/testcases/reverseIntegerInput.txt create mode 100644 backend/question-service/src/scripts/testcases/reverseIntegerOutput.txt diff --git a/backend/question-service/src/scripts/README.md b/backend/question-service/src/scripts/README.md new file mode 100644 index 0000000000..4940ac4a73 --- /dev/null +++ b/backend/question-service/src/scripts/README.md @@ -0,0 +1,6 @@ +# Question Service Seed + +Questions, test cases, code templates, and solutions are generated using ChatGPT. + +Disclaimer: They may not be fully accurate. + diff --git a/backend/question-service/src/scripts/seed.ts b/backend/question-service/src/scripts/seed.ts index 427003d35e..ccf07d4433 100644 --- a/backend/question-service/src/scripts/seed.ts +++ b/backend/question-service/src/scripts/seed.ts @@ -130,6 +130,27 @@ export async function seedQuestions() { javaTemplate: `import java.util.*;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n String s = scanner.nextLine().trim();\n int numRows = Integer.parseInt(scanner.nextLine().trim());\n System.out.println(solution(s, numRows));\n }\n\n // Write your code here\n public static String solution(String s, int numRows) {\n // Implement your solution here\n return "";\n }\n}`, cTemplate: `#include \n#include \n\n// Function to implement\nconst char* solution(const char* s, int numRows) {\n // Implement your solution here\n return "";\n}\n\n// Please do not modify the main function\nint main() {\n char s[1000];\n int numRows;\n fgets(s, sizeof(s), stdin);\n s[strcspn(s, "\\n")] = 0; // Remove newline\n scanf("%d", &numRows);\n printf("%s\\n", solution(s, numRows));\n return 0;\n}`, }, + { + title: "Reverse Integer", + description: + "Given a signed 32-bit integer `x`, return `x` with its digits reversed.\n\n" + + "If reversing `x` causes the value to go outside the signed 32-bit integer range `[-2^31, 2^31 - 1]`, then return 0.\n\n" + + "### Input\n" + + "Each test case consists of a single integer `x`.\n\n" + + "### Output\n" + + "For each test case, output a single integer which is the reversed integer, or 0 if the result overflows.\n\n" + + "### Explanation\n" + + "- **Test Case 1**: `x = 123` reverses to `321`.\n" + + "- **Test Case 2**: `x = -123` reverses to `-321`.\n" + + "- **Test Case 3**: `x = 120` reverses to `21`.\n", + complexity: "Easy", + category: ["Strings"], + testcaseInputFileUrl: "./src/scripts/testcases/reverseIntegerInput.txt", + testcaseOutputFileUrl: "./src/scripts/testcases/reverseIntegerOutput.txt", + pythonTemplate: `# Please do not modify the main function\ndef main():\n\tx = int(input().strip())\n\tprint(solution(x))\n\n\n# Write your code here\ndef solution(x):\n\t# Implement your solution here\n\treturn 0\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `import java.util.Scanner;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n int x = scanner.nextInt();\n System.out.println(solution(x));\n }\n\n // Write your code here\n public static int solution(int x) {\n // Implement your solution here\n return 0;\n }\n}`, + cTemplate: `#include \n#include \n\n// Function to implement\nint solution(int x) {\n // Implement your solution here\n return 0;\n}\n\n// Please do not modify the main function\nint main() {\n int x;\n scanf("%d", &x);\n printf("%d\\n", solution(x));\n return 0;\n}`, + }, ]; try { diff --git a/backend/question-service/src/scripts/testcases/longestPalindromicSubstring.py b/backend/question-service/src/scripts/testcases/longestPalindromicSubstring.py new file mode 100644 index 0000000000..275a6a738d --- /dev/null +++ b/backend/question-service/src/scripts/testcases/longestPalindromicSubstring.py @@ -0,0 +1,34 @@ +# Please do not modify the main function +def main(): + s = input().strip() + print(solution(s)) + + +# Write your code here +def solution(s): + # Helper function to expand around the center and find palindromic substrings + def expand_around_center(left, right): + while left >= 0 and right < len(s) and s[left] == s[right]: + left -= 1 + right += 1 + # Return the longest palindrome substring from the current center + return s[left + 1 : right] + + longest_palindrome = "" + + for i in range(len(s)): + # Check for odd-length palindromes + odd_palindrome = expand_around_center(i, i) + # Check for even-length palindromes + even_palindrome = expand_around_center(i, i + 1) + + # Update the longest palindrome if a longer one is found + longest_palindrome = max( + longest_palindrome, odd_palindrome, even_palindrome, key=len + ) + + return longest_palindrome + + +if __name__ == "__main__": + main() diff --git a/backend/question-service/src/scripts/testcases/longestSubstring.py b/backend/question-service/src/scripts/testcases/longestSubstring.py new file mode 100644 index 0000000000..59c1fa9c9c --- /dev/null +++ b/backend/question-service/src/scripts/testcases/longestSubstring.py @@ -0,0 +1,26 @@ +# Please do not modify the main function +def main(): + s = input().strip() + print(solution(s)) + + +# Write your code here +def solution(s): + char_map = {} + max_len = 0 + start = 0 + + for i, char in enumerate(s): + # If the character is already in the substring, update the start position + if char in char_map and char_map[char] >= start: + start = char_map[char] + 1 + # Update the character's latest position + char_map[char] = i + # Update the max length if the current substring length is greater + max_len = max(max_len, i - start + 1) + + return max_len + + +if __name__ == "__main__": + main() diff --git a/backend/question-service/src/scripts/testcases/medianTwoSortedArray.py b/backend/question-service/src/scripts/testcases/medianTwoSortedArray.py new file mode 100644 index 0000000000..7968ff9867 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/medianTwoSortedArray.py @@ -0,0 +1,27 @@ +# Please do not modify the main function +def main(): + import sys + + input = sys.stdin.read().strip().split("\n\n") + for case in input: + lines = case.split("\n") + nums1 = list(map(int, lines[0].split())) + nums2 = list(map(int, lines[1].split())) + print(f"{solution(nums1, nums2):.1f}") + + +# Write your code here +def solution(nums1, nums2): + # Merge the two sorted arrays + merged = sorted(nums1 + nums2) + n = len(merged) + + # Calculate the median + if n % 2 == 1: # Odd number of elements + return merged[n // 2] + else: # Even number of elements + return (merged[n // 2 - 1] + merged[n // 2]) / 2 + + +if __name__ == "__main__": + main() diff --git a/backend/question-service/src/scripts/testcases/reverseInteger.py b/backend/question-service/src/scripts/testcases/reverseInteger.py new file mode 100644 index 0000000000..ed7c09f4c1 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/reverseInteger.py @@ -0,0 +1,27 @@ +# Please do not modify the main function +def main(): + x = int(input().strip()) + print(solution(x)) + + +# Write your code here +def solution(x): + # Define the 32-bit integer boundaries + INT_MIN, INT_MAX = -(2**31), 2**31 - 1 + + # Check if x is negative and handle reversal accordingly + sign = -1 if x < 0 else 1 + x *= sign + + # Reverse the integer by converting to string, reversing, and converting back to int + reversed_x = int(str(x)[::-1]) * sign + + # Check for overflow + if reversed_x < INT_MIN or reversed_x > INT_MAX: + return 0 + + return reversed_x + + +if __name__ == "__main__": + main() diff --git a/backend/question-service/src/scripts/testcases/reverseIntegerInput.txt b/backend/question-service/src/scripts/testcases/reverseIntegerInput.txt new file mode 100644 index 0000000000..6a8e6d66af --- /dev/null +++ b/backend/question-service/src/scripts/testcases/reverseIntegerInput.txt @@ -0,0 +1,5 @@ +123 + +-123 + +120 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/reverseIntegerOutput.txt b/backend/question-service/src/scripts/testcases/reverseIntegerOutput.txt new file mode 100644 index 0000000000..f43f1438d0 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/reverseIntegerOutput.txt @@ -0,0 +1,5 @@ +321 + +-321 + +21 \ No newline at end of file From 1356a66207649f00e04cf7d432b29721450ed5d1 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 5 Nov 2024 23:56:40 +0800 Subject: [PATCH 106/192] Shift end session logic to collab context --- .../src/handlers/websocketHandler.ts | 5 ++-- .../src/handlers/websocketHandler.ts | 7 +---- frontend/src/contexts/CollabContext.tsx | 24 +++++++++++++-- frontend/src/contexts/MatchContext.tsx | 30 ------------------- frontend/src/pages/CollabSandbox/index.tsx | 9 ++++-- frontend/src/utils/collabSocket.ts | 5 ++-- frontend/src/utils/constants.ts | 3 +- 7 files changed, 37 insertions(+), 46 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 3ab030f59d..ee2948d436 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -18,8 +18,7 @@ enum CollabEvents { DOCUMENT_READY = "document_ready", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", - // PARTNER_LEFT = "partner_left", - // PARTNER_DISCONNECTED = "partner_disconnected", + PARTNER_LEFT = "partner_left", } const EXPIRY_TIME = 3600; @@ -179,5 +178,7 @@ const handleUserLeave = (uid: string, roomId: string, socket: Socket) => { const room = io.sockets.adapter.rooms.get(roomId); if (!room || room.size === 0) { removeCollabSession(roomId); + } else { + io.to(roomId).emit(CollabEvents.PARTNER_LEFT); } }; diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index 2093b5de48..a738530bfc 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -32,7 +32,6 @@ enum MatchEvents { MATCH_FOUND = "match_found", MATCH_SUCCESSFUL = "match_successful", MATCH_UNSUCCESSFUL = "match_unsuccessful", - MATCH_ENDED = "match_ended", MATCH_REQUEST_EXISTS = "match_request_exists", MATCH_REQUEST_ERROR = "match_request_error", } @@ -199,10 +198,7 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { socket.on(MatchEvents.MATCH_END_REQUEST, (uid: string, matchId: string) => { userConnections.delete(uid); - const matchDeleted = handleMatchDelete(matchId); - if (matchDeleted) { - socket.to(matchId).emit(MatchEvents.MATCH_ENDED); - } + handleMatchDelete(matchId); }); socket.on( @@ -257,7 +253,6 @@ const endMatchOnUserDisconnect = (socket: Socket, uid: string) => { const matchDeleted = handleMatchDelete(matchId); if (matchDeleted) { socket.to(matchId).emit(MatchEvents.MATCH_UNSUCCESSFUL); // on matching page - socket.to(matchId).emit(MatchEvents.MATCH_ENDED); // on collab page } } }; diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index c5ff38c1e8..364c600926 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -6,6 +6,7 @@ import { FAILED_TESTCASE_MESSAGE, SUCCESS_TESTCASE_MESSAGE, FAILED_TO_SUBMIT_CODE_MESSAGE, + COLLAB_ENDED_MESSAGE, } from "../utils/constants"; import { toast } from "react-toastify"; @@ -14,9 +15,10 @@ import { codeExecutionClient } from "../utils/api"; import { useReducer } from "react"; import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; -import { leave } from "../utils/collabSocket"; +import { CollabEvents, collabSocket, leave } from "../utils/collabSocket"; import { CommunicationEvents } from "../components/Chat"; import { communicationSocket } from "../utils/communicationSocket"; +import useAppNavigate from "../components/UseAppNavigate"; type CompilerResult = { status: string; @@ -35,14 +37,17 @@ type CollabContextType = { handleEndSessionClick: () => void; handleRejectEndSession: () => void; handleConfirmEndSession: () => void; + checkPartnerStatus: () => void; setCode: React.Dispatch>; compilerResult: CompilerResult[]; + isEndSessionModalOpen: boolean; }; const CollabContext = createContext(null); const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const { children } = props; + const appNavigate = useAppNavigate(); const match = useMatch(); @@ -58,7 +63,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { stopMatch, questionId, qnHistoryId, - setIsEndSessionModalOpen, } = match; // eslint-disable-next-line @@ -68,6 +72,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { ); const [code, setCode] = useState(""); const [compilerResult, setCompilerResult] = useState([]); + const [isEndSessionModalOpen, setIsEndSessionModalOpen] = + useState(false); const handleSubmitSessionClick = async (time: number) => { try { @@ -135,8 +141,18 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { partner?.username ); - // End match + // Delete match data stopMatch(); + appNavigate("/home"); + }; + + const checkPartnerStatus = () => { + collabSocket.on(CollabEvents.PARTNER_LEFT, () => { + toast.error(COLLAB_ENDED_MESSAGE); + setIsEndSessionModalOpen(false); + stopMatch(); + appNavigate("/home"); + }); }; return ( @@ -146,8 +162,10 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { handleEndSessionClick, handleRejectEndSession, handleConfirmEndSession, + checkPartnerStatus, setCode, compilerResult, + isEndSessionModalOpen, }} > {children} diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 38281ddb0c..4ac0b64308 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -6,7 +6,6 @@ import { ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE, FAILED_MATCH_REQUEST_MESSAGE, MATCH_CONNECTION_ERROR, - MATCH_ENDED_MESSAGE, MATCH_LOGIN_REQUIRED_MESSAGE, MATCH_REQUEST_EXISTS_MESSAGE, MATCH_UNSUCCESSFUL_MESSAGE, @@ -18,9 +17,6 @@ import useAppNavigate from "../components/UseAppNavigate"; import { UNSAFE_NavigationContext } from "react-router-dom"; import { Action, type History, type Transition } from "history"; -import { useReducer } from "react"; -import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; - let matchUserId: string; let partnerUserId: string; @@ -54,7 +50,6 @@ enum MatchEvents { MATCH_FOUND = "match_found", MATCH_SUCCESSFUL = "match_successful", MATCH_UNSUCCESSFUL = "match_unsuccessful", - MATCH_ENDED = "match_ended", MATCH_REQUEST_EXISTS = "match_request_exists", MATCH_REQUEST_ERROR = "match_request_error", @@ -93,8 +88,6 @@ type MatchContextType = { partner: MatchUser | null; matchPending: boolean; loading: boolean; - isEndSessionModalOpen: boolean; - setIsEndSessionModalOpen: React.Dispatch>; questionId: string | null; qnHistoryId: string | null; }; @@ -124,15 +117,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [questionId, setQuestionId] = useState(null); const [qnHistoryId, setQnHistoryId] = useState(null); - const [isEndSessionModalOpen, setIsEndSessionModalOpen] = - useState(false); - - // eslint-disable-next-line - const [qnHistoryState, qnHistoryDispatch] = useReducer( - qnHistoryReducer, - initialQHState - ); - const navigator = useContext(UNSAFE_NavigationContext).navigator as History; useEffect(() => { @@ -233,9 +217,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { case MatchPaths.MATCHED: initMatchedListeners(); return; - case MatchPaths.COLLAB: - initCollabListeners(); - return; default: return; } @@ -318,14 +299,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); }; - const initCollabListeners = () => { - matchSocket.on(MatchEvents.MATCH_ENDED, () => { - toast.error(MATCH_ENDED_MESSAGE); - setIsEndSessionModalOpen(false); - appNavigate(MatchPaths.HOME); - }); - }; - const handleMatchFound = ( matchId: string, user1: MatchUser, @@ -409,7 +382,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { return; case MatchPaths.COLLAB: matchSocket.emit(MatchEvents.MATCH_END_REQUEST, matchUser?.id, matchId); - appNavigate(MatchPaths.HOME); return; default: return; @@ -539,8 +511,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { partner, matchPending, loading, - isEndSessionModalOpen, - setIsEndSessionModalOpen, questionId, qnHistoryId, }} diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 55f14d6c3d..f617506b9b 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -76,7 +76,6 @@ const CollabSandbox: React.FC = () => { partner, matchCriteria, loading, - isEndSessionModalOpen, questionId, } = match; @@ -85,7 +84,12 @@ const CollabSandbox: React.FC = () => { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { handleRejectEndSession, handleConfirmEndSession } = collab; + const { + handleRejectEndSession, + handleConfirmEndSession, + checkPartnerStatus, + isEndSessionModalOpen, + } = collab; const [state, dispatch] = useReducer(reducer, initialState); const { selectedQuestion } = state; @@ -110,6 +114,7 @@ const CollabSandbox: React.FC = () => { const editorState = await join(matchUser.id, matchId); if (editorState.ready) { setEditorState(editorState); + checkPartnerStatus(); } else { toast.error(COLLAB_CONNECTION_ERROR); setIsConnecting(false); diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 94d8cdbf17..4fe50bafe3 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -4,7 +4,7 @@ import { updateCursor, Cursor } from "./collabCursor"; import { Doc, Text, applyUpdateV2 } from "yjs"; import { Awareness } from "y-protocols/awareness"; -enum CollabEvents { +export enum CollabEvents { // Send JOIN = "join", LEAVE = "leave", @@ -18,6 +18,7 @@ enum CollabEvents { DOCUMENT_READY = "document_ready", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", + PARTNER_LEFT = "partner_left", SOCKET_DISCONNECT = "disconnect", SOCKET_CLIENT_DISCONNECT = "io client disconnect", SOCKET_SERVER_DISCONNECT = "io server disconnect", @@ -32,7 +33,7 @@ export type CollabSessionData = { }; const COLLAB_SOCKET_URL = "http://localhost:3003"; -const collabSocket = io(COLLAB_SOCKET_URL, { +export const collabSocket = io(COLLAB_SOCKET_URL, { reconnectionAttempts: 3, autoConnect: false, }); diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 9438cd0c3c..92e54cba55 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -92,7 +92,6 @@ export const FAILED_MATCH_REQUEST_MESSAGE = "Failed to send match request! Please try again from the home page."; export const MATCH_UNSUCCESSFUL_MESSAGE = "Unfortunately, your partner did not accept the match."; -export const MATCH_ENDED_MESSAGE = "Your partner has left the match."; export const MATCH_LOGIN_REQUIRED_MESSAGE = "Please login first to find a match."; export const MATCH_OFFER_TIMEOUT_MESSAGE = "Match offer timeout!"; @@ -102,6 +101,8 @@ export const QUESTION_DOES_NOT_EXIST_ERROR = "There are no questions with the specified complexity and category. Please try another combination."; // Collab +export const COLLAB_ENDED_MESSAGE = + "Your partner has left the collaboration session."; export const COLLAB_CONNECTION_ERROR = "Error connecting you to the collaboration session! Please try again."; From ead61dfe3ef69db944c43c68b58818030329d5f2 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:59:04 +0800 Subject: [PATCH 107/192] More answers --- backend/question-service/src/scripts/seed.ts | 41 +++++++++++- .../src/scripts/testcases/addBinary.py | 34 ++++++++++ .../src/scripts/testcases/addBinaryInput.txt | 5 ++ .../src/scripts/testcases/addBinaryOutput.txt | 3 + .../scripts/testcases/binaryTreeMaxPathSum.py | 65 +++++++++++++++++++ .../testcases/binaryTreeMaxPathSumInput.txt | 3 + .../testcases/binaryTreeMaxPathSumOutput.txt | 3 + 7 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 backend/question-service/src/scripts/testcases/addBinary.py create mode 100644 backend/question-service/src/scripts/testcases/addBinaryInput.txt create mode 100644 backend/question-service/src/scripts/testcases/addBinaryOutput.txt create mode 100644 backend/question-service/src/scripts/testcases/binaryTreeMaxPathSum.py create mode 100644 backend/question-service/src/scripts/testcases/binaryTreeMaxPathSumInput.txt create mode 100644 backend/question-service/src/scripts/testcases/binaryTreeMaxPathSumOutput.txt diff --git a/backend/question-service/src/scripts/seed.ts b/backend/question-service/src/scripts/seed.ts index ccf07d4433..6fdfb71440 100644 --- a/backend/question-service/src/scripts/seed.ts +++ b/backend/question-service/src/scripts/seed.ts @@ -54,7 +54,7 @@ export async function seedQuestions() { description: "Given a string `s`, find the length of the **longest substring** without repeating characters.", complexity: "Medium", - category: ["Strings"], + category: ["Strings", "Algorithms"], testcaseInputFileUrl: "./src/scripts/testcases/longestSubstringInput.txt", testcaseOutputFileUrl: "./src/scripts/testcases/longestSubstringOutput.txt", @@ -151,6 +151,45 @@ export async function seedQuestions() { javaTemplate: `import java.util.Scanner;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n int x = scanner.nextInt();\n System.out.println(solution(x));\n }\n\n // Write your code here\n public static int solution(int x) {\n // Implement your solution here\n return 0;\n }\n}`, cTemplate: `#include \n#include \n\n// Function to implement\nint solution(int x) {\n // Implement your solution here\n return 0;\n}\n\n// Please do not modify the main function\nint main() {\n int x;\n scanf("%d", &x);\n printf("%d\\n", solution(x));\n return 0;\n}`, }, + { + title: "Add Binary", + description: + "Given two binary strings `a` and `b`, return their sum as a binary string.\n\n" + + "Each test case consists of two lines:\n" + + "- The first line contains the binary string `a`.\n" + + "- The second line contains the binary string `b`.\n\n" + + "### Explanation\n" + + '- **Test Case 1**: `a = "11"` and `b = "1"`, the sum is `"100"`.\n' + + '- **Test Case 2**: `a = "1010"` and `b = "1011"`, the sum is `"10101"`.', + complexity: "Easy", + category: ["Data Structures", "Strings"], + testcaseInputFileUrl: "./src/scripts/testcases/addBinaryInput.txt", + testcaseOutputFileUrl: "./src/scripts/testcases/addBinaryOutput.txt", + pythonTemplate: `# Please do not modify the main function\ndef main():\n\ta = input().strip()\n\tb = input().strip()\n\tprint(solution(a, b))\n\n\n# Write your code here\ndef solution(a, b):\n\t# Implement your solution here\n\treturn ""\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `import java.util.Scanner;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n String a = scanner.nextLine().trim();\n String b = scanner.nextLine().trim();\n System.out.println(solution(a, b));\n }\n\n // Write your code here\n public static String solution(String a, String b) {\n // Implement your solution here\n return "";\n }\n}`, + cTemplate: `#include \n#include \n\n// Function to implement\nconst char* solution(const char* a, const char* b) {\n // Implement your solution here\n return "";\n}\n\n// Please do not modify the main function\nint main() {\n char a[1000], b[1000];\n fgets(a, sizeof(a), stdin);\n fgets(b, sizeof(b), stdin);\n // Remove newline from input if exists\n a[strcspn(a, "\\n")] = 0;\n b[strcspn(b, "\\n")] = 0;\n printf("%s\\n", solution(a, b));\n return 0;\n}`, + }, + { + title: "Binary Tree Maximum Path Sum", + description: + "Given the `root` of a binary tree, return the **maximum path sum** of any **non-empty path**.\n\n" + + "A path in a binary tree is defined as a sequence of nodes where each pair of adjacent nodes in the sequence has an edge connecting them.\n" + + "A node can only appear once in the path, and the path does not necessarily need to pass through the root.\n\n" + + "### Explanation\n" + + "- ![image](https://assets.leetcode.com/uploads/2020/10/13/exx1.jpg)\n" + + "- **Test Case 1**: Given the tree `[1, 2, 3]`, the maximum path sum is `6` (2 → 1 → 3).\n\n\n" + + "- ![image](https://assets.leetcode.com/uploads/2020/10/13/exx2.jpg)\n" + + "- **Test Case 2**: Given the tree `[-10, 9, 20, null, null, 15, 7]`, the maximum path sum is `42` (15 → 20 → 7).", + complexity: "Hard", + category: ["Tree", "Dynamic Programming", "Recursion"], + testcaseInputFileUrl: + "./src/scripts/testcases/binaryTreeMaxPathSumInput.txt", + testcaseOutputFileUrl: + "./src/scripts/testcases/binaryTreeMaxPathSumOutput.txt", + pythonTemplate: `# Definition for a binary tree node.\nclass TreeNode:\n\tdef __init__(self, val=0, left=None, right=None):\n\t\tself.val = val\n\t\tself.left = left\n\t\tself.right = right\n\n# Please do not modify the main function\ndef main():\n\troot = deserialize(input().strip())\n\tprint(solution(root))\n\n\n# Write your code here\ndef solution(root):\n\t# Implement your solution here\n\treturn 0\n\n\n# Helper function to deserialize input\n\ndef deserialize(data):\n\t# Implement a function to build a tree from input\n\treturn None\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `import java.util.*;\n\nclass TreeNode {\n\tint val;\n\tTreeNode left;\n\tTreeNode right;\n\tTreeNode(int val) { this.val = val; }\n\tTreeNode(int val, TreeNode left, TreeNode right) {\n\t\tthis.val = val;\n\t\tthis.left = left;\n\t\tthis.right = right;\n\t}\n}\n\npublic class Main {\n\t// Please do not modify the main function\n\tpublic static void main(String[] args) {\n\t\tTreeNode root = deserialize(new Scanner(System.in).nextLine().trim());\n\t\tSystem.out.println(solution(root));\n\t}\n\n\t// Write your code here\n\tpublic static int solution(TreeNode root) {\n\t\t// Implement your solution here\n\t\treturn 0;\n\t}\n\n\t// Helper function to deserialize input\n\tpublic static TreeNode deserialize(String data) {\n\t\t// Implement a function to build a tree from input\n\t\treturn null;\n\t}\n}`, + cTemplate: `#include \n#include \n\nstruct TreeNode {\n\tint val;\n\tstruct TreeNode *left;\n\tstruct TreeNode *right;\n};\n\n// Function to implement\nint solution(struct TreeNode* root) {\n\t// Implement your solution here\n\treturn 0;\n}\n\n// Please do not modify the main function\nint main() {\n\t// Implement tree deserialization if needed\n\treturn 0;\n}`, + }, ]; try { diff --git a/backend/question-service/src/scripts/testcases/addBinary.py b/backend/question-service/src/scripts/testcases/addBinary.py new file mode 100644 index 0000000000..f091ac7570 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/addBinary.py @@ -0,0 +1,34 @@ +# Please do not modify the main function +def main(): + a = input().strip() + b = input().strip() + print(solution(a, b)) + + +# Write your code here +def solution(a, b): + # Make sure a is the longer string + if len(b) > len(a): + a, b = b, a + + # Initialize result and carry + result = [] + carry = 0 + b = b.zfill(len(a)) # Pad the shorter string with leading zeros + + # Add binary numbers from the end to the start + for i in range(len(a) - 1, -1, -1): + sum = int(a[i]) + int(b[i]) + carry + result.append(str(sum % 2)) # Append the current binary digit + carry = sum // 2 # Calculate the carry + + # If there’s a carry left after the final addition, add it + if carry: + result.append("1") + + # Join and reverse result since we appended in reverse order + return "".join(result[::-1]) + + +if __name__ == "__main__": + main() diff --git a/backend/question-service/src/scripts/testcases/addBinaryInput.txt b/backend/question-service/src/scripts/testcases/addBinaryInput.txt new file mode 100644 index 0000000000..889a6ff652 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/addBinaryInput.txt @@ -0,0 +1,5 @@ +11 +1 + +1010 +1011 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/addBinaryOutput.txt b/backend/question-service/src/scripts/testcases/addBinaryOutput.txt new file mode 100644 index 0000000000..45c3dd34a2 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/addBinaryOutput.txt @@ -0,0 +1,3 @@ +100 + +10101 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/binaryTreeMaxPathSum.py b/backend/question-service/src/scripts/testcases/binaryTreeMaxPathSum.py new file mode 100644 index 0000000000..0afbf00c3d --- /dev/null +++ b/backend/question-service/src/scripts/testcases/binaryTreeMaxPathSum.py @@ -0,0 +1,65 @@ +# Definition for a binary tree node. +class TreeNode: + def __init__(self, val=0, left=None, right=None): + self.val = val + self.left = left + self.right = right + + +# Please do not modify the main function +def main(): + root = deserialize(input().strip()) + print(solution(root)) + + +# Write your code here +def solution(root): + def max_gain(node): + nonlocal max_sum + if not node: + return 0 + + # Recursively calculate the maximum path sum of left and right subtrees + left_gain = max(max_gain(node.left), 0) # Ignore negative paths + right_gain = max(max_gain(node.right), 0) # Ignore negative paths + + # Current node max path sum includes both left and right contributions + current_path_sum = node.val + left_gain + right_gain + + # Update the maximum path sum found so far + max_sum = max(max_sum, current_path_sum) + + # Return the max gain if we continue the same path with this node + return node.val + max(left_gain, right_gain) + + max_sum = float("-inf") + max_gain(root) + return max_sum + + +# Helper function to deserialize input in space-separated format +def deserialize(data): + if not data: + return None + + nodes = data.split() + root = TreeNode(int(nodes[0])) + queue = [root] + i = 1 + + while queue and i < len(nodes): + node = queue.pop(0) + if nodes[i] != "null": + node.left = TreeNode(int(nodes[i])) + queue.append(node.left) + i += 1 + if i < len(nodes) and nodes[i] != "null": + node.right = TreeNode(int(nodes[i])) + queue.append(node.right) + i += 1 + + return root + + +if __name__ == "__main__": + main() diff --git a/backend/question-service/src/scripts/testcases/binaryTreeMaxPathSumInput.txt b/backend/question-service/src/scripts/testcases/binaryTreeMaxPathSumInput.txt new file mode 100644 index 0000000000..763c279fde --- /dev/null +++ b/backend/question-service/src/scripts/testcases/binaryTreeMaxPathSumInput.txt @@ -0,0 +1,3 @@ +1 2 3 + +-10 9 20 null null 15 7 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/binaryTreeMaxPathSumOutput.txt b/backend/question-service/src/scripts/testcases/binaryTreeMaxPathSumOutput.txt new file mode 100644 index 0000000000..3e9157fbae --- /dev/null +++ b/backend/question-service/src/scripts/testcases/binaryTreeMaxPathSumOutput.txt @@ -0,0 +1,3 @@ +6 + +42 \ No newline at end of file From 853ecff543c443bc2cf43996efed53fdf3923833 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Wed, 6 Nov 2024 00:20:59 +0800 Subject: [PATCH 108/192] More questions and answers --- backend/question-service/src/scripts/seed.ts | 38 ++++++++++++- .../testcases/binaryTreeInorderTraversal.py | 55 +++++++++++++++++++ .../binaryTreeInorderTraversalInput.txt | 3 + .../binaryTreeInorderTraversalOutput.txt | 3 + .../src/scripts/testcases/jumpGame.py | 20 +++++++ .../src/scripts/testcases/jumpGameInput.txt | 3 + .../src/scripts/testcases/jumpGameOutput.txt | 3 + frontend/src/contexts/CollabContext.tsx | 1 + 8 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 backend/question-service/src/scripts/testcases/binaryTreeInorderTraversal.py create mode 100644 backend/question-service/src/scripts/testcases/binaryTreeInorderTraversalInput.txt create mode 100644 backend/question-service/src/scripts/testcases/binaryTreeInorderTraversalOutput.txt create mode 100644 backend/question-service/src/scripts/testcases/jumpGame.py create mode 100644 backend/question-service/src/scripts/testcases/jumpGameInput.txt create mode 100644 backend/question-service/src/scripts/testcases/jumpGameOutput.txt diff --git a/backend/question-service/src/scripts/seed.ts b/backend/question-service/src/scripts/seed.ts index 6fdfb71440..9ced8aada1 100644 --- a/backend/question-service/src/scripts/seed.ts +++ b/backend/question-service/src/scripts/seed.ts @@ -162,7 +162,7 @@ export async function seedQuestions() { '- **Test Case 1**: `a = "11"` and `b = "1"`, the sum is `"100"`.\n' + '- **Test Case 2**: `a = "1010"` and `b = "1011"`, the sum is `"10101"`.', complexity: "Easy", - category: ["Data Structures", "Strings"], + category: ["Data Structures", "Strings", "Bit Manipulation"], testcaseInputFileUrl: "./src/scripts/testcases/addBinaryInput.txt", testcaseOutputFileUrl: "./src/scripts/testcases/addBinaryOutput.txt", pythonTemplate: `# Please do not modify the main function\ndef main():\n\ta = input().strip()\n\tb = input().strip()\n\tprint(solution(a, b))\n\n\n# Write your code here\ndef solution(a, b):\n\t# Implement your solution here\n\treturn ""\n\n\nif __name__ == "__main__":\n\tmain()\n`, @@ -190,6 +190,42 @@ export async function seedQuestions() { javaTemplate: `import java.util.*;\n\nclass TreeNode {\n\tint val;\n\tTreeNode left;\n\tTreeNode right;\n\tTreeNode(int val) { this.val = val; }\n\tTreeNode(int val, TreeNode left, TreeNode right) {\n\t\tthis.val = val;\n\t\tthis.left = left;\n\t\tthis.right = right;\n\t}\n}\n\npublic class Main {\n\t// Please do not modify the main function\n\tpublic static void main(String[] args) {\n\t\tTreeNode root = deserialize(new Scanner(System.in).nextLine().trim());\n\t\tSystem.out.println(solution(root));\n\t}\n\n\t// Write your code here\n\tpublic static int solution(TreeNode root) {\n\t\t// Implement your solution here\n\t\treturn 0;\n\t}\n\n\t// Helper function to deserialize input\n\tpublic static TreeNode deserialize(String data) {\n\t\t// Implement a function to build a tree from input\n\t\treturn null;\n\t}\n}`, cTemplate: `#include \n#include \n\nstruct TreeNode {\n\tint val;\n\tstruct TreeNode *left;\n\tstruct TreeNode *right;\n};\n\n// Function to implement\nint solution(struct TreeNode* root) {\n\t// Implement your solution here\n\treturn 0;\n}\n\n// Please do not modify the main function\nint main() {\n\t// Implement tree deserialization if needed\n\treturn 0;\n}`, }, + { + title: "Jump Game", + description: + "Given an array of non-negative integers `nums`, where each element represents the maximum jump length from that position, determine if you can reach the last index.\n\n" + + "Return string `true` if you can reach the last index, or string `false` otherwise.\n\n" + + "### Explanation\n" + + "- **Test Case 1**: Given `nums = [2, 3, 1, 1, 4]`, you can jump from index 0 to index 1, then to the last index, so the result is `true`.\n" + + "- **Test Case 2**: Given `nums = [3, 2, 1, 0, 4]`, no matter what, you will always end up at index 3 (0), so the result is `false`.", + complexity: "Medium", + category: ["Algorithms", "Arrays", "Dynamic Programming"], + testcaseInputFileUrl: "./src/scripts/testcases/jumpGameInput.txt", + testcaseOutputFileUrl: "./src/scripts/testcases/jumpGameOutput.txt", + pythonTemplate: `# Please do not modify the main function\ndef main():\n\tnums = list(map(int, input().strip().split()))\n\tprint(solution(nums))\n\n\n# Write your code here\ndef solution(nums):\n\t# Implement your solution here\n\treturn False\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `import java.util.*;\n\npublic class Main {\n // Please do not modify the main function\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n List numsList = new ArrayList<>();\n while (scanner.hasNextInt()) {\n numsList.add(scanner.nextInt());\n }\n int[] nums = numsList.stream().mapToInt(i -> i).toArray();\n System.out.println(solution(nums));\n }\n\n // Write your code here\n public static boolean solution(int[] nums) {\n // Implement your solution here\n return false;\n }\n}`, + cTemplate: `#include \n#include \n\n// Function to implement\nbool solution(int* nums, int numsSize) {\n // Implement your solution here\n return false;\n}\n\n// Please do not modify the main function\nint main() {\n int nums[1000], numsSize = 0, x;\n while (scanf("%d", &x) == 1) {\n nums[numsSize++] = x;\n }\n printf(solution(nums, numsSize) ? "true\\n" : "false\\n");\n return 0;\n}`, + }, + { + title: "Binary Tree Inorder Traversal", + description: + "Given the `root` of a binary tree, return the **inorder traversal** of its nodes' values.\n\n" + + "Inorder traversal is defined as visiting the left subtree, then the current node, and finally the right subtree.\n\n" + + "### Explanation\n" + + "- ![image](https://assets.leetcode.com/uploads/2024/08/29/screenshot-2024-08-29-202743.png)\n" + + "- **Test Case 1**: Given the tree `[1, null, 2, 3]`, the inorder traversal is `[1, 3, 2]`.\n\n\n" + + "- ![image](https://assets.leetcode.com/uploads/2024/08/29/tree_2.png)\n" + + "- **Test Case 2**: Given the tree `[1, 2, 3, 4, 5, null, 8 ,null, null, 6, 7, 9]`, the inorder traversal is `[4, 2, 6, 5, 7, 1, 3, 9, 8]`.", + complexity: "Easy", + category: ["Tree"], + testcaseInputFileUrl: + "./src/scripts/testcases/binaryTreeInorderTraversalInput.txt", + testcaseOutputFileUrl: + "./src/scripts/testcases/binaryTreeInorderTraversalOutput.txt", + pythonTemplate: `# Definition for a binary tree node.\nclass TreeNode:\n\tdef __init__(self, val=0, left=None, right=None):\n\t\tself.val = val\n\t\tself.left = left\n\t\tself.right = right\n\n# Please do not modify the main function\ndef main():\n\troot = deserialize(input().strip())\n\tprint(" ".join(map(str, solution(root))))\n\n\n# Write your code here\ndef solution(root):\n\t# Implement your solution here\n\treturn []\n\n\n# Helper function to deserialize input\n\ndef deserialize(data):\n\t# Implement a function to build a tree from input\n\treturn None\n\n\nif __name__ == "__main__":\n\tmain()\n`, + javaTemplate: `import java.util.*;\n\nclass TreeNode {\n\tint val;\n\tTreeNode left;\n\tTreeNode right;\n\tTreeNode(int val) { this.val = val; }\n\tTreeNode(int val, TreeNode left, TreeNode right) {\n\t\tthis.val = val;\n\t\tthis.left = left;\n\t\tthis.right = right;\n\t}\n}\n\npublic class Main {\n\t// Please do not modify the main function\n\tpublic static void main(String[] args) {\n\t\tTreeNode root = deserialize(new Scanner(System.in).nextLine().trim());\n\t\tList result = solution(root);\n\t\tSystem.out.println(String.join(" ", result.stream().map(String::valueOf).toArray(String[]::new)));\n\t}\n\n\t// Write your code here\n\tpublic static List solution(TreeNode root) {\n\t\t// Implement your solution here\n\t\treturn new ArrayList<>();\n\t}\n\n\t// Helper function to deserialize input\n\tpublic static TreeNode deserialize(String data) {\n\t\t// Implement a function to build a tree from input\n\t\treturn null;\n\t}\n}`, + cTemplate: `#include \n#include \n\nstruct TreeNode {\n\tint val;\n\tstruct TreeNode *left;\n\tstruct TreeNode *right;\n};\n\n// Function to implement\nint* solution(struct TreeNode* root, int* returnSize) {\n\t// Implement your solution here\n\t*returnSize = 0;\n\treturn NULL;\n}\n\n// Please do not modify the main function\nint main() {\n\t// Implement tree deserialization if needed\n\treturn 0;\n}`, + }, ]; try { diff --git a/backend/question-service/src/scripts/testcases/binaryTreeInorderTraversal.py b/backend/question-service/src/scripts/testcases/binaryTreeInorderTraversal.py new file mode 100644 index 0000000000..0c9fa66abc --- /dev/null +++ b/backend/question-service/src/scripts/testcases/binaryTreeInorderTraversal.py @@ -0,0 +1,55 @@ +# Definition for a binary tree node. +class TreeNode: + def __init__(self, val=0, left=None, right=None): + self.val = val + self.left = left + self.right = right + + +# Please do not modify the main function +def main(): + root = deserialize(input().strip()) + print(solution(root)) + + +# Write your code here +def solution(root): + result = [] + + def inorder(node): + if not node: + return + inorder(node.left) + result.append(node.val) + inorder(node.right) + + inorder(root) + return result + + +# Helper function to deserialize input in space-separated format +def deserialize(data): + if not data: + return None + + nodes = data.split() + root = TreeNode(int(nodes[0])) + queue = [root] + i = 1 + + while queue and i < len(nodes): + node = queue.pop(0) + if nodes[i] != "null": + node.left = TreeNode(int(nodes[i])) + queue.append(node.left) + i += 1 + if i < len(nodes) and nodes[i] != "null": + node.right = TreeNode(int(nodes[i])) + queue.append(node.right) + i += 1 + + return root + + +if __name__ == "__main__": + main() diff --git a/backend/question-service/src/scripts/testcases/binaryTreeInorderTraversalInput.txt b/backend/question-service/src/scripts/testcases/binaryTreeInorderTraversalInput.txt new file mode 100644 index 0000000000..86b81e1abb --- /dev/null +++ b/backend/question-service/src/scripts/testcases/binaryTreeInorderTraversalInput.txt @@ -0,0 +1,3 @@ +1 null 2 3 + +1 2 3 4 5 null 8 null null 6 7 9 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/binaryTreeInorderTraversalOutput.txt b/backend/question-service/src/scripts/testcases/binaryTreeInorderTraversalOutput.txt new file mode 100644 index 0000000000..c8f34f8fcd --- /dev/null +++ b/backend/question-service/src/scripts/testcases/binaryTreeInorderTraversalOutput.txt @@ -0,0 +1,3 @@ +1 3 2 + +4 2 6 5 7 1 3 9 8 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/jumpGame.py b/backend/question-service/src/scripts/testcases/jumpGame.py new file mode 100644 index 0000000000..44adbe2941 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/jumpGame.py @@ -0,0 +1,20 @@ +# Please do not modify the main function +def main(): + nums = list(map(int, input().strip().split())) + print(solution(nums)) + + +# Write your code here +def solution(nums): + max_reach = 0 + for i in range(len(nums)): + if i > max_reach: + return "false" + max_reach = max(max_reach, i + nums[i]) + if max_reach >= len(nums) - 1: + return "true" + return "false" + + +if __name__ == "__main__": + main() diff --git a/backend/question-service/src/scripts/testcases/jumpGameInput.txt b/backend/question-service/src/scripts/testcases/jumpGameInput.txt new file mode 100644 index 0000000000..4aa9168dc5 --- /dev/null +++ b/backend/question-service/src/scripts/testcases/jumpGameInput.txt @@ -0,0 +1,3 @@ +2 3 1 1 4 + +3 2 1 0 4 \ No newline at end of file diff --git a/backend/question-service/src/scripts/testcases/jumpGameOutput.txt b/backend/question-service/src/scripts/testcases/jumpGameOutput.txt new file mode 100644 index 0000000000..ad1723bd6f --- /dev/null +++ b/backend/question-service/src/scripts/testcases/jumpGameOutput.txt @@ -0,0 +1,3 @@ +true + +false \ No newline at end of file diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 44f8ed2cd6..ce867edde3 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -77,6 +77,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { language: matchCriteria?.language.toLowerCase(), }); + console.log(res.data.data); setCompilerResult(res.data.data); let isMatch = true; From b431b11e2ac880b9ab7e702633c6a5ce91b320a2 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 01:33:04 +0800 Subject: [PATCH 109/192] Change separator for test files --- .../src/controllers/questionController.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index 08d04bc5f1..aa8a6d6347 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -398,10 +398,10 @@ const formatQuestionResponse = (question: IQuestion) => { }; const formatQuestionIndivResponse = async (question: IQuestion) => { - const testcaseDelimiter = "\n"; - const inputs = (await getFileContent(question.testcaseInputFileUrl)).split( - testcaseDelimiter, - ); + const testcaseDelimiter = "\n\n"; + const inputs = (await getFileContent(question.testcaseInputFileUrl)) + .replace(/\r\n/g, "\n") + .split(testcaseDelimiter); const outputs = (await getFileContent(question.testcaseOutputFileUrl)).split( testcaseDelimiter, ); From 4593412f18a7d263f84be3f61359096b72d2be47 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 02:03:37 +0800 Subject: [PATCH 110/192] Update code execution --- .../controllers/codeExecutionControllers.ts | 14 ++++++------- .../tests/codeExecutionRoutes.spec.ts | 20 ++++++------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index 4b2aca6cd4..75dae66aa2 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -41,17 +41,17 @@ export const executeCode = async (req: Request, res: Response) => { try { // Get question test case files const qnsResponse = await questionService.get(`/${questionId}`); - const { testcaseInputFileUrl, testcaseOutputFileUrl } = + const { inputs: stdinList, outputs: expectedResultList } = qnsResponse.data.question; // Extract test cases from input and output files - const testCases = await testCasesApi( - testcaseInputFileUrl, - testcaseOutputFileUrl - ); + // const testCases = await testCasesApi( + // testcaseInputFileUrl, + // testcaseOutputFileUrl + // ); - const stdinList: string[] = testCases.input; - const expectedResultList: string[] = testCases.output; + // const stdinList: string[] = testCases.input; + // const expectedResultList: string[] = testCases.output; if (stdinList.length !== expectedResultList.length) { res.status(400).json({ diff --git a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts index 55764f4614..3b5d916225 100644 --- a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts +++ b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts @@ -57,17 +57,12 @@ describe("Code execution routes", () => { (questionService.get as jest.Mock).mockResolvedValue({ data: { question: { - testcaseInputFileUrl: "https://peerprep.com/input", - testcaseOutputFileUrl: "https://peerprep.com/output", + inputs: ["1", "2"], + outputs: ["1"], }, }, }); - (testCasesApi as jest.Mock).mockResolvedValue({ - input: ["1", "2"], - output: ["1"], - }); - const response = await request.post(`${BASE_URL}/run`).send({ language: "python", code: "print('Hello, world!')", @@ -81,23 +76,20 @@ describe("Code execution routes", () => { (questionService.get as jest.Mock).mockResolvedValue({ data: { question: { - testcaseInputFileUrl: "https://peerprep.com/input", - testcaseOutputFileUrl: "https://peerprep.com/output", + inputs: ["1", "2"], + outputs: ["1", "2"], }, }, }); - (testCasesApi as jest.Mock).mockResolvedValue({ - input: ["1", "2"], - output: ["1", "4"], - }); - const response = await request.post(`${BASE_URL}/run`).send({ language: "python", code: "print(input())", questionId: "1234", }); + console.log(response.body); + expect(response.status).toBe(200); expect(response.body.message).toBe(SUCCESS_MESSAGE); expect(response.body.data).toBeInstanceOf(Array); From 30e825807f65146ee5261c3b600196ac695b677b Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 02:06:13 +0800 Subject: [PATCH 111/192] Remove extra QuestionDetail type and --- frontend/src/reducers/questionReducer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts index 25a5471e07..1300e8dbd2 100644 --- a/frontend/src/reducers/questionReducer.ts +++ b/frontend/src/reducers/questionReducer.ts @@ -58,7 +58,7 @@ enum QuestionActionTypes { type QuestionActions = { type: QuestionActionTypes; - payload: QuestionList | QuestionDetail | QuestionDetail | string[] | string; + payload: QuestionList | QuestionDetail | string[] | string; }; type QuestionsState = { From ca07d5e11a68971d7e8495b5be9a000fb4fe63a8 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 02:06:41 +0800 Subject: [PATCH 112/192] Fix testcases output --- .../question-service/src/controllers/questionController.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index aa8a6d6347..da9dab9bd6 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -402,9 +402,9 @@ const formatQuestionIndivResponse = async (question: IQuestion) => { const inputs = (await getFileContent(question.testcaseInputFileUrl)) .replace(/\r\n/g, "\n") .split(testcaseDelimiter); - const outputs = (await getFileContent(question.testcaseOutputFileUrl)).split( - testcaseDelimiter, - ); + const outputs = (await getFileContent(question.testcaseOutputFileUrl)) + .replace(/\r\n/g, "\n") + .split(testcaseDelimiter); return { id: question._id, title: question.title, From d9fb7683095b2021920727372ca7a50c92ccae93 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Wed, 6 Nov 2024 08:40:01 +0800 Subject: [PATCH 113/192] Change configs for aws eb --- .ebextensions/01-launch-template.config | 4 ++++ .gitignore | 5 +++++ package.json | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .ebextensions/01-launch-template.config diff --git a/.ebextensions/01-launch-template.config b/.ebextensions/01-launch-template.config new file mode 100644 index 0000000000..d2e4f5cdc9 --- /dev/null +++ b/.ebextensions/01-launch-template.config @@ -0,0 +1,4 @@ +option_settings: + aws:autoscaling:launchconfiguration: + RootVolumeType: gp3 + DisableIMDSv1: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 84f98a5642..ff2ce92680 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,8 @@ coverage # Environment files .env + +# Elastic Beanstalk Files +.elasticbeanstalk/* +!.elasticbeanstalk/*.cfg.yml +!.elasticbeanstalk/*.global.yml diff --git a/package.json b/package.json index df5486e833..78cba92a9f 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,6 @@ "husky": "^9.1.6" }, "scripts": { - "prepare": "husky" + "prepare": "command -v husky && husky || true" } } From 3ca60ea8d57da4f6f2e949e0064a804b16a398f0 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 10:23:48 +0800 Subject: [PATCH 114/192] Close communication socket when user ends session --- frontend/src/contexts/CollabContext.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 9afba34ac0..2392ed44bb 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -16,7 +16,6 @@ import { useReducer } from "react"; import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; import { CollabEvents, collabSocket, leave } from "../utils/collabSocket"; -import { CommunicationEvents } from "../components/Chat"; import { communicationSocket } from "../utils/communicationSocket"; import useAppNavigate from "../components/UseAppNavigate"; @@ -131,16 +130,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { leave(partner?.id as string, getMatchId() as string, true); // Leave chat room - communicationSocket.emit( - CommunicationEvents.LEAVE, - getMatchId(), - matchUser?.username - ); - communicationSocket.emit( - CommunicationEvents.LEAVE, - getMatchId(), - partner?.username - ); + communicationSocket.disconnect(); // Delete match data stopMatch(); From 798819c542bcb8e20e8d5fc08015c5b890edc871 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:26:24 +0800 Subject: [PATCH 115/192] Update code execution --- frontend/src/components/TestCase/index.tsx | 62 +++++++++++++++------- frontend/src/contexts/CollabContext.tsx | 13 +++-- frontend/src/pages/CollabSandbox/index.tsx | 18 ++++--- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/TestCase/index.tsx b/frontend/src/components/TestCase/index.tsx index a430d67225..07c8394ed3 100644 --- a/frontend/src/components/TestCase/index.tsx +++ b/frontend/src/components/TestCase/index.tsx @@ -1,10 +1,10 @@ import { Box, styled, Typography } from "@mui/material"; +import { CompilerResult } from "../../contexts/CollabContext"; type TestCaseProps = { input: string; - output?: string; - stdout: string; - result?: string; + expected: string; + result: CompilerResult; }; const StyledBox = styled(Box)(({ theme }) => ({ @@ -18,34 +18,58 @@ const StyledTypography = styled(Typography)(({ theme }) => ({ whiteSpace: "pre-line", })); -const TestCase: React.FC = ({ - input, - output, - stdout, - result, -}) => { +const TestCase: React.FC = ({ input, expected, result }) => { return ( - ({ marginBottom: theme.spacing(2) })}> + {"isMatch" in result && result.isMatch && ( + + + Accepted + + + )} + {"isMatch" in result && !result.isMatch && ( + + + Wrong Answer + + + )} + {result.stderr && ( + + Error + + {result.stderr} + + + )} + Input {input} - {output && ( + + Expected + {expected} + + {"actualResult" in result && ( - Output - {output} + Actual + + {result.actualResult || "\u00A0"} + )} - {stdout && ( + {"stdout" in result && ( Stdout - {stdout} + + {result.stdout || "\u00A0"} + )} - - Expected - {result} - ); }; diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 9afba34ac0..aa2b583e80 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -20,7 +20,7 @@ import { CommunicationEvents } from "../components/Chat"; import { communicationSocket } from "../utils/communicationSocket"; import useAppNavigate from "../components/UseAppNavigate"; -type CompilerResult = { +export type CompilerResult = { status: string; exception: string | null; stdout: string; @@ -30,6 +30,7 @@ type CompilerResult = { stout: string; actualResult: string; expectedResult: string; + isMatch: boolean; }; type CollabContextType = { @@ -40,6 +41,7 @@ type CollabContextType = { checkPartnerStatus: () => void; setCode: React.Dispatch>; compilerResult: CompilerResult[]; + setCompilerResult: React.Dispatch>; isEndSessionModalOpen: boolean; }; @@ -79,12 +81,12 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { try { const res = await codeExecutionClient.post("/", { questionId, - code, + // Replace tabs with 4 spaces to prevent formatting issues + code: code.replace(/\t/g, " ".repeat(4)), language: matchCriteria?.language.toLowerCase(), }); - - console.log(res.data.data); - setCompilerResult(res.data.data); + console.log([...res.data.data]); + setCompilerResult([...res.data.data]); let isMatch = true; for (let i = 0; i < res.data.data.length; i++) { @@ -166,6 +168,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { checkPartnerStatus, setCode, compilerResult, + setCompilerResult, isEndSessionModalOpen, }} > diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 323457dee4..5c27452f42 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -12,7 +12,7 @@ import { Tabs, } from "@mui/material"; import classes from "./index.module.css"; -import { useCollab } from "../../contexts/CollabContext"; +import { CompilerResult, useCollab } from "../../contexts/CollabContext"; import { useMatch } from "../../contexts/MatchContext"; import { COLLAB_CONNECTION_ERROR, @@ -61,6 +61,8 @@ const CollabSandbox: React.FC = () => { } const { + compilerResult, + setCompilerResult, handleRejectEndSession, handleConfirmEndSession, checkPartnerStatus, @@ -79,6 +81,7 @@ const CollabSandbox: React.FC = () => { return; } getQuestionById(questionId, dispatch); + setCompilerResult([]); const matchId = getMatchId(); if (!matchUser || !matchId) { @@ -95,7 +98,7 @@ const CollabSandbox: React.FC = () => { toast.error(COLLAB_CONNECTION_ERROR); setIsConnecting(false); } - } catch (error) { + } catch { toast.error(COLLAB_CONNECTION_ERROR); setIsConnecting(false); } @@ -122,7 +125,7 @@ const CollabSandbox: React.FC = () => { return ; } - if (!selectedQuestion || !editorState) { + if (!selectedQuestion || !editorState || !compilerResult) { return ; } @@ -262,9 +265,12 @@ const CollabSandbox: React.FC = () => { {/* display result of each test case in the output (result) and stdout (any print statements executed) */} 0 + ? compilerResult[selectedTestcase] + : ({} as CompilerResult) + } />
From 58d045fcc55d8ce8e71418f5971da83591dbcfc5 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:25:00 +0800 Subject: [PATCH 116/192] Update question history with current progress when user end session without submitting previously --- .../CollabSessionControls/index.tsx | 16 +----- frontend/src/contexts/CollabContext.tsx | 52 ++++++++++++++++--- frontend/src/pages/CollabSandbox/index.tsx | 5 +- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index a35599d7a8..81b30bdb6a 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -2,26 +2,14 @@ import { Button, Stack } from "@mui/material"; import Stopwatch from "../Stopwatch"; import { useCollab } from "../../contexts/CollabContext"; import { USE_COLLAB_ERROR_MESSAGE } from "../../utils/constants"; -import { useEffect, useState } from "react"; const CollabSessionControls: React.FC = () => { - const [time, setTime] = useState(0); - - useEffect(() => { - const intervalId = setInterval( - () => setTime((prevTime) => prevTime + 1), - 1000 - ); - - return () => clearInterval(intervalId); - }, [time]); - const collab = useCollab(); if (!collab) { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { handleSubmitSessionClick, handleEndSessionClick } = collab; + const { handleSubmitSessionClick, handleEndSessionClick, time } = collab; return ( @@ -33,7 +21,7 @@ const CollabSessionControls: React.FC = () => { }} variant="outlined" color="success" - onClick={() => handleSubmitSessionClick(time)} + onClick={() => handleSubmitSessionClick()} > Submit diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 7d81ed2d03..372b127338 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-refresh/only-export-components */ -import React, { createContext, useContext, useState } from "react"; +import React, { createContext, useContext, useEffect, useState } from "react"; import { USE_MATCH_ERROR_MESSAGE, FAILED_TESTCASE_MESSAGE, @@ -11,7 +11,7 @@ import { import { toast } from "react-toastify"; import { useMatch } from "./MatchContext"; -import { codeExecutionClient } from "../utils/api"; +import { qnHistoryClient, codeExecutionClient } from "../utils/api"; import { useReducer } from "react"; import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; @@ -33,15 +33,16 @@ export type CompilerResult = { }; type CollabContextType = { - handleSubmitSessionClick: (time: number) => void; + handleSubmitSessionClick: () => void; handleEndSessionClick: () => void; handleRejectEndSession: () => void; handleConfirmEndSession: () => void; checkPartnerStatus: () => void; setCode: React.Dispatch>; compilerResult: CompilerResult[]; - setCompilerResult: React.Dispatch>; isEndSessionModalOpen: boolean; + time: number; + resetCollab: () => void; }; const CollabContext = createContext(null); @@ -66,6 +67,17 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { qnHistoryId, } = match; + const [time, setTime] = useState(0); + + useEffect(() => { + const intervalId = setInterval( + () => setTime((prevTime) => prevTime + 1), + 1000 + ); + + return () => clearInterval(intervalId); + }, [time]); + // eslint-disable-next-line const [_qnHistoryState, qnHistoryDispatch] = useReducer( qnHistoryReducer, @@ -76,7 +88,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [isEndSessionModalOpen, setIsEndSessionModalOpen] = useState(false); - const handleSubmitSessionClick = async (time: number) => { + const handleSubmitSessionClick = async () => { try { const res = await codeExecutionClient.post("/", { questionId, @@ -124,9 +136,26 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setIsEndSessionModalOpen(false); }; - const handleConfirmEndSession = () => { + const handleConfirmEndSession = async () => { setIsEndSessionModalOpen(false); + // Get queston history + const data = await qnHistoryClient.get(qnHistoryId as string); + + // Only update question history if it has not been submitted before + if (!data.data.qnHistory.code) { + updateQnHistoryById( + qnHistoryId as string, + { + submissionStatus: "Attempted", + dateAttempted: new Date().toISOString(), + timeTaken: time, + code: code.replace(/\t/g, " ".repeat(4)), + }, + qnHistoryDispatch + ); + } + // Leave collaboration room leave(matchUser?.id as string, getMatchId() as string, true); leave(partner?.id as string, getMatchId() as string, true); @@ -137,6 +166,9 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { // Delete match data stopMatch(); appNavigate("/home"); + + // Reset collab state + resetCollab(); }; const checkPartnerStatus = () => { @@ -148,6 +180,11 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); }; + const resetCollab = () => { + setCompilerResult([]); + setTime(0); + }; + return ( = (props) => { checkPartnerStatus, setCode, compilerResult, - setCompilerResult, isEndSessionModalOpen, + time, + resetCollab, }} > {children} diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 5c27452f42..cadaa8095c 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -62,11 +62,11 @@ const CollabSandbox: React.FC = () => { const { compilerResult, - setCompilerResult, handleRejectEndSession, handleConfirmEndSession, checkPartnerStatus, isEndSessionModalOpen, + resetCollab, } = collab; const [state, dispatch] = useReducer(reducer, initialState); @@ -81,7 +81,8 @@ const CollabSandbox: React.FC = () => { return; } getQuestionById(questionId, dispatch); - setCompilerResult([]); + + resetCollab(); const matchId = getMatchId(); if (!matchUser || !matchId) { From d97c0496d958836564337435cfc499e917e1b99b Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 15:03:44 +0800 Subject: [PATCH 117/192] Fix linting --- frontend/src/components/Chat/index.tsx | 20 +++++-------------- .../QuestionCodeTemplates/index.tsx | 1 - frontend/src/utils/communicationSocket.ts | 13 ++++++++++++ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 64fffcdd1b..b834158704 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -1,6 +1,9 @@ import { Box, styled, TextField, Typography } from "@mui/material"; import { useEffect, useRef, useState } from "react"; -import { communicationSocket } from "../../utils/communicationSocket"; +import { + CommunicationEvents, + communicationSocket, +} from "../../utils/communicationSocket"; import { useMatch } from "../../contexts/MatchContext"; import { USE_AUTH_ERROR_MESSAGE, @@ -15,19 +18,6 @@ type Message = { createdTime: number; }; -export enum CommunicationEvents { - // receive - JOIN = "join", - SEND_TEXT_MESSAGE = "send_text_message", - DISCONNECT = "disconnect", - - // send - USER_JOINED = "user_joined", - ALREADY_JOINED = "already_joined", - TEXT_MESSAGE_RECEIVED = "text_message_received", - DISCONNECTED = "disconnected", -} - type ChatProps = { isActive: boolean; }; @@ -66,13 +56,13 @@ const Chat: React.FC = ({ isActive }) => { roomId: getMatchId(), username: user?.username, }); - // eslint-disable-next-line react-hooks/exhaustive-deps return () => { console.log("closing socket..."); communicationSocket.close(); setMessages([]); // clear the earlier messages in dev mode }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { diff --git a/frontend/src/components/QuestionCodeTemplates/index.tsx b/frontend/src/components/QuestionCodeTemplates/index.tsx index 16651fce79..b5ee81fa3b 100644 --- a/frontend/src/components/QuestionCodeTemplates/index.tsx +++ b/frontend/src/components/QuestionCodeTemplates/index.tsx @@ -66,7 +66,6 @@ const QuestionCodeTemplates: React.FC = ({ event.target.selectionEnd = cursorPosition + 1; } }; - /* eslint-enable @typescript-eslint/no-explicit-any */ return ( diff --git a/frontend/src/utils/communicationSocket.ts b/frontend/src/utils/communicationSocket.ts index ebda964f0b..46d179558d 100644 --- a/frontend/src/utils/communicationSocket.ts +++ b/frontend/src/utils/communicationSocket.ts @@ -1,5 +1,18 @@ import { io } from "socket.io-client"; +export enum CommunicationEvents { + // receive + JOIN = "join", + SEND_TEXT_MESSAGE = "send_text_message", + DISCONNECT = "disconnect", + + // send + USER_JOINED = "user_joined", + ALREADY_JOINED = "already_joined", + TEXT_MESSAGE_RECEIVED = "text_message_received", + DISCONNECTED = "disconnected", +} + const COMMUNICATION_SOCKET_URL = "http://localhost:3005"; export const communicationSocket = io(COMMUNICATION_SOCKET_URL, { From 705469655e9b327b2fd8373610467a549ea1314f Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 15:20:40 +0800 Subject: [PATCH 118/192] Fix linting --- .../controllers/codeExecutionControllers.ts | 1 - .../src/utils/testCasesApi.ts | 20 ------------------- .../tests/codeExecutionRoutes.spec.ts | 1 - 3 files changed, 22 deletions(-) delete mode 100644 backend/code-execution-service/src/utils/testCasesApi.ts diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index bc7306979a..1e3cf8cd62 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -10,7 +10,6 @@ import { ERROR_INVALID_TEST_CASES_MESSAGE, } from "../utils/constants"; import { questionService } from "../utils/questionApi"; -import { testCasesApi } from "../utils/testCasesApi"; interface CompilerResult { status: string; diff --git a/backend/code-execution-service/src/utils/testCasesApi.ts b/backend/code-execution-service/src/utils/testCasesApi.ts deleted file mode 100644 index 7146fe7b11..0000000000 --- a/backend/code-execution-service/src/utils/testCasesApi.ts +++ /dev/null @@ -1,20 +0,0 @@ -import axios from "axios"; - -export const testCasesApi = async ( - inputFileUrl: string, - outputFileUrl: string -) => { - try { - const inputFileUrlResponse = await axios.get(inputFileUrl); - const outputFileUrlResponse = await axios.get(outputFileUrl); - - // Split the input and output files by double new line - return { - input: inputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"), - output: outputFileUrlResponse.data.replace(/\r\n/g, "\n").split("\n\n"), - }; - } catch { - console.log("Failed to fetch test cases"); - return { input: [], output: [] }; - } -}; diff --git a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts index 3b5d916225..b5e8ce3ca1 100644 --- a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts +++ b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts @@ -6,7 +6,6 @@ import { ERROR_NOT_SAME_LENGTH_MESSAGE, SUCCESS_MESSAGE, } from "../src/utils/constants"; -import { testCasesApi } from "../src/utils/testCasesApi"; import { questionService } from "../src/utils/questionApi"; const request = supertest(app); From 0237d8640fbaee6fab1626fc2decd53e1b8b7aa3 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 15:47:59 +0800 Subject: [PATCH 119/192] Remove testcasesApi --- .../code-execution-service/tests/codeExecutionRoutes.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts index b5e8ce3ca1..e1ab51161f 100644 --- a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts +++ b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts @@ -18,10 +18,6 @@ jest.mock("../src/utils/questionApi", () => ({ }, })); -jest.mock("../src/utils/testCasesApi", () => ({ - testCasesApi: jest.fn(), -})); - describe("Code execution routes", () => { beforeEach(() => { jest.clearAllMocks(); From a08f41bcd997e81798d27fc596389d4909ec2f94 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 16:00:16 +0800 Subject: [PATCH 120/192] Remove unused code --- .../src/controllers/codeExecutionControllers.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts index 1e3cf8cd62..7dd72daa56 100644 --- a/backend/code-execution-service/src/controllers/codeExecutionControllers.ts +++ b/backend/code-execution-service/src/controllers/codeExecutionControllers.ts @@ -43,15 +43,6 @@ export const executeCode = async (req: Request, res: Response) => { const { inputs: stdinList, outputs: expectedResultList } = qnsResponse.data.question; - // Extract test cases from input and output files - // const testCases = await testCasesApi( - // testcaseInputFileUrl, - // testcaseOutputFileUrl - // ); - - // const stdinList: string[] = testCases.input; - // const expectedResultList: string[] = testCases.output; - if (stdinList.length !== expectedResultList.length) { res.status(400).json({ message: ERROR_NOT_SAME_LENGTH_MESSAGE, From bd320d35fe7a8ff20efbbbb56ac09b0ae5bd36d1 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 17:54:58 +0800 Subject: [PATCH 121/192] Close socket connection after a delay --- .../src/handlers/userConnectionHandler.ts | 25 +++++++++++++++++++ .../src/handlers/websocketHandler.ts | 11 ++++---- .../communication-service/src/utils/types.ts | 6 +++++ frontend/src/components/Chat/index.tsx | 6 ++--- frontend/src/contexts/CollabContext.tsx | 9 ++++--- frontend/src/contexts/MatchContext.tsx | 2 +- .../index.tsx => hooks/useAppNavigate.tsx} | 0 .../debounce.ts => hooks/useDebounce.tsx} | 2 +- frontend/src/pages/CollabSandbox/index.tsx | 1 + frontend/src/pages/QuestionList/index.tsx | 4 +-- frontend/src/utils/communicationSocket.ts | 5 ++-- 11 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 backend/communication-service/src/handlers/userConnectionHandler.ts rename frontend/src/{components/UseAppNavigate/index.tsx => hooks/useAppNavigate.tsx} (100%) rename frontend/src/{utils/debounce.ts => hooks/useDebounce.tsx} (97%) diff --git a/backend/communication-service/src/handlers/userConnectionHandler.ts b/backend/communication-service/src/handlers/userConnectionHandler.ts new file mode 100644 index 0000000000..6dc58d3d52 --- /dev/null +++ b/backend/communication-service/src/handlers/userConnectionHandler.ts @@ -0,0 +1,25 @@ +import { Socket } from "socket.io"; +import { UserConnection } from "../utils/types"; + +const userConnections: Map = new Map(); + +const delay = 3000; + +export const disconnectUser = (socket: Socket) => { + const { username } = socket.data; + const userConnection = userConnections.get(username); + clearTimeout(userConnection?.timeout); + const timeout = setTimeout(() => { + console.log("DISCONNECTING: ", socket.data); + userConnections.delete(username); + socket.disconnect(); + }, delay); + userConnections.set(username, { timeout }); +}; + +export const connectUser = (username: string) => { + console.log("CONNECTING: ", username); + const userConnection = userConnections.get(username); + clearTimeout(userConnection?.timeout); + userConnections.set(username, {}); +}; diff --git a/backend/communication-service/src/handlers/websocketHandler.ts b/backend/communication-service/src/handlers/websocketHandler.ts index c61fc27c6f..92658abdf2 100644 --- a/backend/communication-service/src/handlers/websocketHandler.ts +++ b/backend/communication-service/src/handlers/websocketHandler.ts @@ -2,15 +2,13 @@ import { Socket } from "socket.io"; import { CommunicationEvents, MessageTypes } from "../utils/types"; import { BOT_NAME } from "../utils/constants"; import { io } from "../server"; +import { connectUser, disconnectUser } from "./userConnectionHandler"; export const handleWebsocketCommunicationEvents = (socket: Socket) => { socket.on( CommunicationEvents.JOIN, async ({ roomId, username }: { roomId: string; username: string }) => { - if (!roomId) { - return; - } - + connectUser(username); const room = io.sockets.adapter.rooms.get(roomId); if (room?.has(socket.id)) { socket.emit(CommunicationEvents.ALREADY_JOINED); @@ -55,10 +53,13 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { } ); + socket.on(CommunicationEvents.USER_DISCONNECT, () => { + disconnectUser(socket); + }); + socket.on(CommunicationEvents.DISCONNECT, () => { const { roomId } = socket.data; if (roomId) { - console.log("disconnected", roomId, socket.data.username); const createdTime = Date.now(); socket.to(roomId).emit(CommunicationEvents.DISCONNECTED, { from: BOT_NAME, diff --git a/backend/communication-service/src/utils/types.ts b/backend/communication-service/src/utils/types.ts index 59c993b1f3..a457a0e2f3 100644 --- a/backend/communication-service/src/utils/types.ts +++ b/backend/communication-service/src/utils/types.ts @@ -2,12 +2,14 @@ export enum CommunicationEvents { // receive JOIN = "join", SEND_TEXT_MESSAGE = "send_text_message", + USER_DISCONNECT = "user_disconnect", DISCONNECT = "disconnect", // send USER_JOINED = "user_joined", ALREADY_JOINED = "already_joined", TEXT_MESSAGE_RECEIVED = "text_message_received", + USER_DISCONNECTED = "user_disconnected", DISCONNECTED = "disconnected", } @@ -15,3 +17,7 @@ export enum MessageTypes { USER_GENERATED = "user_generated", BOT_GENERATED = "bot_generated", } + +export type UserConnection = { + timeout?: NodeJS.Timeout; +}; diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index b834158704..ed2ccb0003 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -51,16 +51,14 @@ const Chat: React.FC = ({ isActive }) => { useEffect(() => { // join the room automatically when this loads communicationSocket.open(); - // to make sure this does not run twice communicationSocket.emit(CommunicationEvents.JOIN, { roomId: getMatchId(), username: user?.username, }); return () => { - console.log("closing socket..."); - communicationSocket.close(); - setMessages([]); // clear the earlier messages in dev mode + communicationSocket.emit(CommunicationEvents.USER_DISCONNECT); + // setMessages([]); // clear the earlier messages in dev mode }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 622d42e857..052eed02b5 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -16,8 +16,11 @@ import { useReducer } from "react"; import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; import { CollabEvents, collabSocket, leave } from "../utils/collabSocket"; -import { communicationSocket } from "../utils/communicationSocket"; -import useAppNavigate from "../components/UseAppNavigate"; +import { + CommunicationEvents, + communicationSocket, +} from "../utils/communicationSocket"; +import useAppNavigate from "../hooks/useAppNavigate"; export type CompilerResult = { status: string; @@ -162,7 +165,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { leave(partner?.id as string, getMatchId() as string, true); // Leave chat room - communicationSocket.disconnect(); + communicationSocket.emit(CommunicationEvents.USER_DISCONNECT); // Delete match data stopMatch(); diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 4ac0b64308..c9f78dcfed 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -13,7 +13,7 @@ import { } from "../utils/constants"; import { useAuth } from "./AuthContext"; import { toast } from "react-toastify"; -import useAppNavigate from "../components/UseAppNavigate"; +import useAppNavigate from "../hooks/useAppNavigate"; import { UNSAFE_NavigationContext } from "react-router-dom"; import { Action, type History, type Transition } from "history"; diff --git a/frontend/src/components/UseAppNavigate/index.tsx b/frontend/src/hooks/useAppNavigate.tsx similarity index 100% rename from frontend/src/components/UseAppNavigate/index.tsx rename to frontend/src/hooks/useAppNavigate.tsx diff --git a/frontend/src/utils/debounce.ts b/frontend/src/hooks/useDebounce.tsx similarity index 97% rename from frontend/src/utils/debounce.ts rename to frontend/src/hooks/useDebounce.tsx index 41d0f3957e..950e9f66de 100644 --- a/frontend/src/utils/debounce.ts +++ b/frontend/src/hooks/useDebounce.tsx @@ -3,7 +3,7 @@ import { Dispatch, SetStateAction, useEffect, useState } from "react"; // Adapted from https://karthikraja555.medium.com/react-js-debouncing-or-delaying-value-change-using-custom-hook-1d5fb2e4fe79 const useDebounce = ( initialState: T, - delay: number, + delay: number ): [T, Dispatch>] => { const [actualValue, setActualValue] = useState(initialState); const [debounceValue, setDebounceValue] = useState(initialState); diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 7a5e38c460..597d6c0e8e 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -67,6 +67,7 @@ const CollabSandbox: React.FC = () => { checkPartnerStatus, isEndSessionModalOpen, resetCollab, + setCompilerResult, } = collab; const [state, dispatch] = useReducer(reducer, initialState); diff --git a/frontend/src/pages/QuestionList/index.tsx b/frontend/src/pages/QuestionList/index.tsx index 21cf275692..da2097adf6 100644 --- a/frontend/src/pages/QuestionList/index.tsx +++ b/frontend/src/pages/QuestionList/index.tsx @@ -35,7 +35,7 @@ import { SUCCESS_QUESTION_DELETE, USE_AUTH_ERROR_MESSAGE, } from "../../utils/constants"; -import useDebounce from "../../utils/debounce"; +import useDebounce from "../../hooks/useDebounce"; import { blue, grey } from "@mui/material/colors"; import { Add, Delete, Edit, MoreVert, Search } from "@mui/icons-material"; import ConfirmationDialog from "../../components/ConfirmationDialog"; @@ -121,7 +121,7 @@ const QuestionList: React.FC = () => { if (state.questionCount % rowsPerPage !== 1 || page === 0) { updateQuestionList(); } else { - setPage(page => page - 1); + setPage((page) => page - 1); } }; diff --git a/frontend/src/utils/communicationSocket.ts b/frontend/src/utils/communicationSocket.ts index 46d179558d..51cc908a10 100644 --- a/frontend/src/utils/communicationSocket.ts +++ b/frontend/src/utils/communicationSocket.ts @@ -1,12 +1,13 @@ import { io } from "socket.io-client"; export enum CommunicationEvents { - // receive + // send JOIN = "join", + USER_DISCONNECT = "user_disconnect", SEND_TEXT_MESSAGE = "send_text_message", DISCONNECT = "disconnect", - // send + // receive USER_JOINED = "user_joined", ALREADY_JOINED = "already_joined", TEXT_MESSAGE_RECEIVED = "text_message_received", From 4e0ab4ba7a90e3003d628cc6c72df56f275c4dc1 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 18:05:14 +0800 Subject: [PATCH 122/192] Remove comment --- frontend/src/pages/CollabSandbox/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 597d6c0e8e..dad27af05b 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -265,7 +265,6 @@ const CollabSandbox: React.FC = () => { ))} - {/* display result of each test case in the output (result) and stdout (any print statements executed) */} Date: Wed, 6 Nov 2024 18:09:19 +0800 Subject: [PATCH 123/192] Update docs --- backend/communication-service/README.md | 10 +++++----- .../docs/{image1.png => images/postman-setup1.png} | Bin .../docs/{image2.png => images/postman-setup2.png} | Bin .../docs/{image3.png => images/postman-setup3.png} | Bin .../docs/{image4.png => images/postman-setup4.png} | Bin 5 files changed, 5 insertions(+), 5 deletions(-) rename backend/communication-service/docs/{image1.png => images/postman-setup1.png} (100%) rename backend/communication-service/docs/{image2.png => images/postman-setup2.png} (100%) rename backend/communication-service/docs/{image3.png => images/postman-setup3.png} (100%) rename backend/communication-service/docs/{image4.png => images/postman-setup4.png} (100%) diff --git a/backend/communication-service/README.md b/backend/communication-service/README.md index b0f30a74e9..c66f10c2e6 100644 --- a/backend/communication-service/README.md +++ b/backend/communication-service/README.md @@ -30,19 +30,19 @@ - Select the `Socket.IO` option and set URL to `http://localhost:3005`. Click `Connect`. - ![image1.png](./docs/image1.png) + ![image1.png](./docs/images/postman-setup1.png) - Add the following events in the `Events` tab and listen to them. - ![image2.png](./docs/image2.png) + ![image2.png](./docs/images/postman-setup2.png) - To send a message, go to the `Message` tab and ensure that your message is being parsed as `JSON`. - ![image3.png](./docs/image3.png) + ![image3.png](./docs/images/postman-setup3.png) - In the `Event name` input, input the correct event name. Click on `Send` to send a message. - ![image4.png](./docs/image4.png) + ![image4.png](./docs/images/postman-setup4.png) ## Events Available @@ -50,5 +50,5 @@ | --------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | **join** | Joins a communication rooms | `roomId` (string): ID of the room.
`username` (string): Username of the user that joined. | **user_joined**: Notify the other user that a new user has joined the room. | | **send_text_message** | Sends a message to the other user | `roomId` (string): ID of the room.
`message` (string): Message to send.
`username` (string): User that sent the message.
`createdTime` (number): Time that the user sent the message. | **text_message_received**: Notify the user that a message is sent | -| **leave** | Leaves the communication room. | `roomId` (string): ID of the room to leave.
`username` (string): User that wants to leave. | **user_left**: To notify the user when one user leaves. | +| **user_disconnect** | User disconnection. | None | **user_left**: To notify the user when one user leaves. | | **disconnect** | Disconnects from the server. | None | **disconnected**: To notify the user when one user gets disconnected from the server. | diff --git a/backend/communication-service/docs/image1.png b/backend/communication-service/docs/images/postman-setup1.png similarity index 100% rename from backend/communication-service/docs/image1.png rename to backend/communication-service/docs/images/postman-setup1.png diff --git a/backend/communication-service/docs/image2.png b/backend/communication-service/docs/images/postman-setup2.png similarity index 100% rename from backend/communication-service/docs/image2.png rename to backend/communication-service/docs/images/postman-setup2.png diff --git a/backend/communication-service/docs/image3.png b/backend/communication-service/docs/images/postman-setup3.png similarity index 100% rename from backend/communication-service/docs/image3.png rename to backend/communication-service/docs/images/postman-setup3.png diff --git a/backend/communication-service/docs/image4.png b/backend/communication-service/docs/images/postman-setup4.png similarity index 100% rename from backend/communication-service/docs/image4.png rename to backend/communication-service/docs/images/postman-setup4.png From d2530d53c6fa5375643fb7ebdd41c4b5070ee310 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 22:30:07 +0800 Subject: [PATCH 124/192] Remove setCompilerResult --- frontend/src/pages/CollabSandbox/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 5c27452f42..3784d3d8ec 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -62,7 +62,6 @@ const CollabSandbox: React.FC = () => { const { compilerResult, - setCompilerResult, handleRejectEndSession, handleConfirmEndSession, checkPartnerStatus, @@ -81,7 +80,6 @@ const CollabSandbox: React.FC = () => { return; } getQuestionById(questionId, dispatch); - setCompilerResult([]); const matchId = getMatchId(); if (!matchUser || !matchId) { From ce31ad8d795dd59376ee3f4e95dd4b226f457d27 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:37:35 +0800 Subject: [PATCH 125/192] Fix sanbox --- frontend/src/pages/CollabSandbox/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index e9b9785a40..c2c96abcc4 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -67,7 +67,6 @@ const CollabSandbox: React.FC = () => { checkPartnerStatus, isEndSessionModalOpen, resetCollab, - setCompilerResult, } = collab; const [state, dispatch] = useReducer(reducer, initialState); From ad3c6c4471f8340082e37330d59dfe3e05074ee5 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 6 Nov 2024 23:51:56 +0800 Subject: [PATCH 126/192] Verify user's auth status --- backend/communication-service/.env.sample | 3 + backend/communication-service/README.md | 6 +- .../docs/images/postman-setup2.png | Bin 61725 -> 71723 bytes .../docs/images/postman-setup3.png | Bin 62029 -> 57274 bytes .../docs/images/postman-setup5.png | Bin 0 -> 62029 bytes .../communication-service/package-lock.json | 79 ++++++++++++++++++ backend/communication-service/package.json | 1 + .../src/handlers/websocketHandler.ts | 1 + backend/communication-service/src/server.ts | 17 +++- .../src/utils/userServiceApi.ts | 15 ++++ docker-compose.yml | 2 + frontend/src/components/Chat/index.tsx | 10 +++ frontend/src/utils/communicationSocket.ts | 5 ++ 13 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 backend/communication-service/docs/images/postman-setup5.png create mode 100644 backend/communication-service/src/utils/userServiceApi.ts diff --git a/backend/communication-service/.env.sample b/backend/communication-service/.env.sample index e5f4ba279e..702bd12d55 100644 --- a/backend/communication-service/.env.sample +++ b/backend/communication-service/.env.sample @@ -3,3 +3,6 @@ SERVER_PORT=3005 # Origins for cors ORIGINS=http://localhost:5173,http://127.0.0.1:5173 + +# Other service APIs +USER_SERVICE_URL=http://user-service:3001/api diff --git a/backend/communication-service/README.md b/backend/communication-service/README.md index c66f10c2e6..f1e24f9098 100644 --- a/backend/communication-service/README.md +++ b/backend/communication-service/README.md @@ -36,7 +36,7 @@ ![image2.png](./docs/images/postman-setup2.png) - - To send a message, go to the `Message` tab and ensure that your message is being parsed as `JSON`. + - Add a valid JWT token in the `Authorization` header. ![image3.png](./docs/images/postman-setup3.png) @@ -44,6 +44,10 @@ ![image4.png](./docs/images/postman-setup4.png) + - To send a message, go to the `Message` tab and ensure that your message is being parsed as `JSON`. + + ![image5.png](./docs/images/postman-setup5.png) + ## Events Available | Event Name | Description | Parameters | Response Event | diff --git a/backend/communication-service/docs/images/postman-setup2.png b/backend/communication-service/docs/images/postman-setup2.png index e422ba04625e742fbf424f552944a9e9d9516e38..d361fb1ce32b52eed71853c695df26e3ed883b5f 100644 GIT binary patch literal 71723 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 literal 61725 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

JW?dF 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 Mi{DMTt>N;203|j~Y5)KL diff --git a/backend/communication-service/docs/images/postman-setup3.png b/backend/communication-service/docs/images/postman-setup3.png index 6ad7ecd408a811bcc352b97f304a1c3d45d59d78..3b0a722fdcbb0e73650b91e24986ed1d32807d2c 100644 GIT binary patch literal 57274 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�pfS(ROQWhi8w7+p;6ohtmuI5W%obCF>$ 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�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 diff --git a/backend/communication-service/docs/images/postman-setup5.png b/backend/communication-service/docs/images/postman-setup5.png new file mode 100644 index 0000000000000000000000000000000000000000..6ad7ecd408a811bcc352b97f304a1c3d45d59d78 GIT binary patch literal 62029 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�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 literal 0 HcmV?d00001 diff --git a/backend/communication-service/package-lock.json b/backend/communication-service/package-lock.json index 20d788705e..11548b5a81 100644 --- a/backend/communication-service/package-lock.json +++ b/backend/communication-service/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "axios": "^1.7.7", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", @@ -2492,6 +2493,23 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2907,6 +2925,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3091,6 +3121,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3948,6 +3987,40 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5869,6 +5942,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/backend/communication-service/package.json b/backend/communication-service/package.json index c7528cb505..08fcfb3a6b 100644 --- a/backend/communication-service/package.json +++ b/backend/communication-service/package.json @@ -14,6 +14,7 @@ "license": "ISC", "description": "", "dependencies": { + "axios": "^1.7.7", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", diff --git a/backend/communication-service/src/handlers/websocketHandler.ts b/backend/communication-service/src/handlers/websocketHandler.ts index 92658abdf2..959c80d64e 100644 --- a/backend/communication-service/src/handlers/websocketHandler.ts +++ b/backend/communication-service/src/handlers/websocketHandler.ts @@ -9,6 +9,7 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { CommunicationEvents.JOIN, async ({ roomId, username }: { roomId: string; username: string }) => { connectUser(username); + console.log(username, roomId); const room = io.sockets.adapter.rooms.get(roomId); if (room?.has(socket.id)) { socket.emit(CommunicationEvents.ALREADY_JOINED); diff --git a/backend/communication-service/src/server.ts b/backend/communication-service/src/server.ts index d3a5e530bd..4c2ebd4140 100644 --- a/backend/communication-service/src/server.ts +++ b/backend/communication-service/src/server.ts @@ -2,16 +2,31 @@ import app, { allowedOrigins } from "./app"; import { createServer } from "http"; import { Server } from "socket.io"; import { handleWebsocketCommunicationEvents } from "./handlers/websocketHandler"; +import { verifyToken } from "./utils/userServiceApi"; const PORT = process.env.SERVICE_PORT || 3005; const server = createServer(app); export const io = new Server(server, { - cors: { origin: allowedOrigins, methods: ["GET", "POST"] }, + cors: { origin: allowedOrigins, methods: ["GET", "POST"], credentials: true }, connectionStateRecovery: {}, }); +io.use((socket, next) => { + const token = + socket.handshake.headers.authorization || socket.handshake.auth.token; + verifyToken(token) + .then(() => { + console.log("Valid credentials"); + next(); + }) + .catch((err) => { + console.error(err); + next(new Error("Unauthorized")); + }); +}); + io.on("connection", handleWebsocketCommunicationEvents); server.listen(PORT, () => { diff --git a/backend/communication-service/src/utils/userServiceApi.ts b/backend/communication-service/src/utils/userServiceApi.ts new file mode 100644 index 0000000000..88442a3b6c --- /dev/null +++ b/backend/communication-service/src/utils/userServiceApi.ts @@ -0,0 +1,15 @@ +import axios from "axios"; + +const USER_SERVICE_URL = + process.env.USER_SERVICE_URL || "http://localhost:3001/api"; + +const userClient = axios.create({ + baseURL: USER_SERVICE_URL, + withCredentials: true, +}); + +export const verifyToken = (token: string | undefined) => { + return userClient.get("/auth/verify-token", { + headers: { authorization: token }, + }); +}; diff --git a/docker-compose.yml b/docker-compose.yml index 9af939d783..1d3067ba03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -95,6 +95,8 @@ services: env_file: ./backend/communication-service/.env ports: - 3005:3005 + depends_on: + - user-service networks: - peerprep-network volumes: diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index ed2ccb0003..7bea1c2770 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -10,6 +10,7 @@ import { USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; import { useAuth } from "../../contexts/AuthContext"; +import { toast } from "react-toastify"; type Message = { from: string; @@ -36,6 +37,7 @@ const Chat: React.FC = ({ isActive }) => { const match = useMatch(); const auth = useAuth(); const messagesRef = useRef(null); + const errorHandledRef = useRef(false); if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); @@ -68,10 +70,17 @@ const Chat: React.FC = ({ isActive }) => { const listener = (message: Message) => { setMessages((prevMessages) => [...prevMessages, message]); }; + const errorListener = () => { + if (!errorHandledRef.current) { + toast.error("Connection error. Please try again."); + errorHandledRef.current = true; + } + }; communicationSocket.on(CommunicationEvents.USER_JOINED, listener); communicationSocket.on(CommunicationEvents.TEXT_MESSAGE_RECEIVED, listener); communicationSocket.on(CommunicationEvents.DISCONNECTED, listener); + communicationSocket.on(CommunicationEvents.CONNECT_ERROR, errorListener); return () => { communicationSocket.off(CommunicationEvents.USER_JOINED, listener); @@ -80,6 +89,7 @@ const Chat: React.FC = ({ isActive }) => { listener ); communicationSocket.off(CommunicationEvents.DISCONNECTED, listener); + communicationSocket.off(CommunicationEvents.CONNECT_ERROR, errorListener); }; }, []); diff --git a/frontend/src/utils/communicationSocket.ts b/frontend/src/utils/communicationSocket.ts index 51cc908a10..cff0006ddb 100644 --- a/frontend/src/utils/communicationSocket.ts +++ b/frontend/src/utils/communicationSocket.ts @@ -11,6 +11,7 @@ export enum CommunicationEvents { USER_JOINED = "user_joined", ALREADY_JOINED = "already_joined", TEXT_MESSAGE_RECEIVED = "text_message_received", + CONNECT_ERROR = "connect_error", DISCONNECTED = "disconnected", } @@ -19,4 +20,8 @@ const COMMUNICATION_SOCKET_URL = "http://localhost:3005"; export const communicationSocket = io(COMMUNICATION_SOCKET_URL, { reconnectionAttempts: 3, autoConnect: false, + withCredentials: true, + auth: { + token: `Bearer ${localStorage.getItem("token")}`, + }, }); From 2f23a0cc0b2f0979e71526bd2e167d791ae95153 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Wed, 6 Nov 2024 11:49:53 +0800 Subject: [PATCH 127/192] Fix end session on page refresh and tab closure --- frontend/src/pages/CollabSandbox/index.tsx | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index c2c96abcc4..7c1da0955d 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -46,10 +46,9 @@ const CollabSandbox: React.FC = () => { } const { - verifyMatchStatus, + // verifyMatchStatus, getMatchId, matchUser, - partner, matchCriteria, loading, questionId, @@ -75,7 +74,8 @@ const CollabSandbox: React.FC = () => { const [selectedTestcase, setSelectedTestcase] = useState(0); useEffect(() => { - verifyMatchStatus(); + // TODO: Retain session on page refresh + // verifyMatchStatus(); if (!questionId) { return; @@ -107,7 +107,14 @@ const CollabSandbox: React.FC = () => { connectToCollabSession(); - return () => leave(matchUser.id, matchId); + // handle page refresh / tab closure + const handleUnload = () => leave(matchUser.id, matchId); + window.addEventListener("unload", handleUnload); + + return () => { + leave(matchUser.id, matchId); + window.removeEventListener("unload", handleUnload); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -116,13 +123,7 @@ const CollabSandbox: React.FC = () => { return ; } - if ( - !matchUser || - !partner || - !matchCriteria || - !getMatchId() || - !isConnecting - ) { + if (!matchUser || !matchCriteria || !getMatchId() || !isConnecting) { return ; } From 4e966055aec0e4cdd66ac46e285266fd46d4a2a9 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:34:01 +0800 Subject: [PATCH 128/192] add filters and sorting for qnhistory --- .../controllers/questionHistoryController.ts | 91 +++++++++++++--- .../qn-history-service/src/utils/constants.ts | 8 +- backend/qn-history-service/swagger.yml | 15 +++ .../tests/qnHistoryRoutes.spec.ts | 24 ++-- frontend/src/pages/CollabSandbox/index.tsx | 1 - frontend/src/pages/Profile/index.tsx | 103 +++++++++++++++++- .../src/pages/QuestionHistoryDetail/index.tsx | 6 +- frontend/src/reducers/qnHistoryReducer.ts | 24 +++- frontend/src/utils/sessionTime.ts | 10 +- 9 files changed, 244 insertions(+), 38 deletions(-) diff --git a/backend/qn-history-service/src/controllers/questionHistoryController.ts b/backend/qn-history-service/src/controllers/questionHistoryController.ts index 701c64221a..eaf6294c76 100644 --- a/backend/qn-history-service/src/controllers/questionHistoryController.ts +++ b/backend/qn-history-service/src/controllers/questionHistoryController.ts @@ -3,8 +3,9 @@ import QnHistory, { IQnHistory } from "../models/QnHistory.ts"; import { MONGO_OBJ_ID_FORMAT, MONGO_OBJ_ID_MALFORMED_MESSAGE, + ORDER_INCORRECT_FORMAT_MESSAGE, PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE, - PAGE_LIMIT_USERID_REQUIRED_MESSAGE, + PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE, QN_HIST_CREATED_MESSAGE, QN_HIST_DELETED_MESSAGE, QN_HIST_NOT_FOUND_MESSAGE, @@ -24,7 +25,9 @@ export const createQnHistory = async ( submissionStatus, dateAttempted, timeTaken, + code, language, + //compilerRes, } = req.body; const newQnHistory = new QnHistory({ @@ -34,7 +37,9 @@ export const createQnHistory = async ( submissionStatus, dateAttempted, timeTaken, + code, language, + //compilerRes, }); await newQnHistory.save(); @@ -109,6 +114,9 @@ type QnHistListParams = { page: string; qnHistLimit: string; userId: string; + title: string; //qn title search keyword + status: string; //submission status + order: string; //entries sort order }; export const readQnHistoryList = async ( @@ -116,38 +124,75 @@ export const readQnHistoryList = async ( res: Response ): Promise => { try { - const { page, qnHistLimit, userId } = req.query; + const { page, qnHistLimit, userId, title, status, order } = req.query; - if (!page || !qnHistLimit || !userId) { - res.status(400).json({ message: PAGE_LIMIT_USERID_REQUIRED_MESSAGE }); + if (!page || !qnHistLimit || !userId || !order) { + res + .status(400) + .json({ message: PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE }); return; } const pageInt = parseInt(page, 10); const qnHistLimitInt = parseInt(qnHistLimit, 10); + const orderInt = parseInt(order, 10); if (pageInt < 1 || qnHistLimitInt < 1) { res.status(400).json({ message: PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE }); return; } + if (!(orderInt == 1 || orderInt == -1)) { + res.status(400).json({ message: ORDER_INCORRECT_FORMAT_MESSAGE }); + } + if (!userId.match(MONGO_OBJ_ID_FORMAT)) { res.status(400).json({ message: MONGO_OBJ_ID_MALFORMED_MESSAGE }); return; } - const filteredQnHistCount = await QnHistory.countDocuments({ - userIds: userId, - }); - const filteredQnHist = await QnHistory.find({ userIds: userId }) - .skip((pageInt - 1) * qnHistLimitInt) - .limit(qnHistLimitInt); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const query: any = {}; - res.status(200).json({ - message: QN_HIST_RETRIEVED_MESSAGE, - qnHistoryCount: filteredQnHistCount, - qnHistories: filteredQnHist.map(formatQnHistoryResponse), - }); + if (title) { + query.title = { $regex: new RegExp(title, "i") }; + } + + if (status) { + query.submissionStatus = { + $in: Array.isArray(status) ? status : [status], + }; + } + + query.userIds = { $in: [userId] }; + + if (orderInt == 1) { + //ascending order + const filteredQnHistCount = await QnHistory.countDocuments(query); + const filteredQnHist = await QnHistory.find(query) + .sort({ dateAttempted: 1 }) + .skip((pageInt - 1) * qnHistLimitInt) + .limit(qnHistLimitInt); + + res.status(200).json({ + message: QN_HIST_RETRIEVED_MESSAGE, + qnHistoryCount: filteredQnHistCount, + qnHistories: filteredQnHist.map(formatQnHistoryResponse), + }); + } else { + //descending order + const filteredQnHistCount = await QnHistory.countDocuments(query); + const filteredQnHist = await QnHistory.find(query) + .sort({ dateAttempted: -1 }) + .skip((pageInt - 1) * qnHistLimitInt) + .limit(qnHistLimitInt); + + res.status(200).json({ + message: QN_HIST_RETRIEVED_MESSAGE, + qnHistoryCount: filteredQnHistCount, + qnHistories: filteredQnHist.map(formatQnHistoryResponse), + }); + } } catch (error) { res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); } @@ -191,5 +236,21 @@ const formatQnHistoryResponse = (qnHistory: IQnHistory) => { timeTaken: qnHistory.timeTaken, code: qnHistory.code, language: qnHistory.language, + //compilerRes: qnHistory.compilerRes.map(formatCompilerRes), }; }; + +/*const formatCompilerRes = (compilerRes: ICompilerRes) => { + return { + status: compilerRes.status, + exception: compilerRes.exception, + stdout: compilerRes.stdout, + stderr: compilerRes.stderr, + executionTime: compilerRes.executionTime, + stdin: compilerRes.stdin, + stout: compilerRes.stdout, + actualResult: compilerRes.actualResult, + expectedResult: compilerRes.expectedResult, + isMatch: compilerRes.isMatch, + }; +};*/ diff --git a/backend/qn-history-service/src/utils/constants.ts b/backend/qn-history-service/src/utils/constants.ts index 8f8fbd3097..c05ca2ba83 100644 --- a/backend/qn-history-service/src/utils/constants.ts +++ b/backend/qn-history-service/src/utils/constants.ts @@ -9,11 +9,13 @@ export const SERVER_ERROR_MESSAGE = "Server error."; export const QN_HIST_RETRIEVED_MESSAGE = "Question history retrieved successfully."; -export const PAGE_LIMIT_USERID_REQUIRED_MESSAGE = - "Page number, question limit per page and userId should be provided."; +export const PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE = + "Page number, question history limit per page, userId and sort order should be provided."; export const PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE = - "Page number and question limit per page should be positive integers."; + "Page number and question history limit per page should be positive integers."; + +export const ORDER_INCORRECT_FORMAT_MESSAGE = "Order should only be -1 or 1."; export const MONGO_OBJ_ID_FORMAT = /^[0-9a-fA-F]{24}$/; diff --git a/backend/qn-history-service/swagger.yml b/backend/qn-history-service/swagger.yml index 5818777a8a..10a229b72c 100644 --- a/backend/qn-history-service/swagger.yml +++ b/backend/qn-history-service/swagger.yml @@ -167,6 +167,21 @@ paths: type: string required: true description: User id of user to retrieve question histories + - in: query + name: title + type: string + required: false + description: Search keywords for question history title + - in: query + name: status + type: string + required: false + description: Filter for question history submission status + - in: query + name: order + type: integer + required: true + description: Order (based on date attempted) to sort question history records by (1 for ascending and -1 for descending). responses: 200: description: Successful Response diff --git a/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts b/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts index 998f17f325..0835e1a937 100644 --- a/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts +++ b/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts @@ -4,7 +4,7 @@ import app from "../src/app"; import { MONGO_OBJ_ID_MALFORMED_MESSAGE, PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE, - PAGE_LIMIT_USERID_REQUIRED_MESSAGE, + PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE, QN_HIST_NOT_FOUND_MESSAGE, } from "../src/utils/constants"; import QnHistory from "../src/models/QnHistory"; @@ -54,7 +54,7 @@ describe("Qn History Routes", () => { it("Reads existing question histories", async () => { const qnHistLimit = 10; const res = await request.get( - `${BASE_URL}?page=1&qnHistLimit=${qnHistLimit}&userId=66f77e9f27ab3f794bdae664` + `${BASE_URL}?page=1&qnHistLimit=${qnHistLimit}&userId=66f77e9f27ab3f794bdae664&order=1` ); expect(res.status).toBe(200); expect(res.body.qnHistories.length).toBeLessThanOrEqual(qnHistLimit); @@ -62,29 +62,31 @@ describe("Qn History Routes", () => { it("Does not read without page", async () => { const res = await request.get( - `${BASE_URL}?qnHistLimit=10&userId=66f77e9f27ab3f794bdae664` + `${BASE_URL}?qnHistLimit=10&userId=66f77e9f27ab3f794bdae664&order=1` ); expect(res.status).toBe(400); - expect(res.body.message).toBe(PAGE_LIMIT_USERID_REQUIRED_MESSAGE); + expect(res.body.message).toBe(PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE); }); it("Does not read without qnHistLimit", async () => { const res = await request.get( - `${BASE_URL}?page=1&userId=66f77e9f27ab3f794bdae664` + `${BASE_URL}?page=1&userId=66f77e9f27ab3f794bdae664&order=1` ); expect(res.status).toBe(400); - expect(res.body.message).toBe(PAGE_LIMIT_USERID_REQUIRED_MESSAGE); + expect(res.body.message).toBe(PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE); }); it("Does not read without userId", async () => { - const res = await request.get(`${BASE_URL}?page=1&qnHistLimit=10`); + const res = await request.get( + `${BASE_URL}?page=1&qnHistLimit=10&order=1` + ); expect(res.status).toBe(400); - expect(res.body.message).toBe(PAGE_LIMIT_USERID_REQUIRED_MESSAGE); + expect(res.body.message).toBe(PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE); }); it("Does not read with negative page", async () => { const res = await request.get( - `${BASE_URL}?page=-1&qnHistLimit=10&userId=66f77e9f27ab3f794bdae664` + `${BASE_URL}?page=-1&qnHistLimit=10&userId=66f77e9f27ab3f794bdae664&order=1` ); expect(res.status).toBe(400); expect(res.body.message).toBe(PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE); @@ -92,7 +94,7 @@ describe("Qn History Routes", () => { it("Does not read with negative qnHistLimit", async () => { const res = await request.get( - `${BASE_URL}?page=1&qnHistLimit=-10&userId=66f77e9f27ab3f794bdae664` + `${BASE_URL}?page=1&qnHistLimit=-10&userId=66f77e9f27ab3f794bdae664&order=1` ); expect(res.status).toBe(400); expect(res.body.message).toBe(PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE); @@ -100,7 +102,7 @@ describe("Qn History Routes", () => { it("Does not read with invalid userId format", async () => { const res = await request.get( - `${BASE_URL}?page=1&qnHistLimit=10&userId=6` + `${BASE_URL}?page=1&qnHistLimit=10&userId=6&order=1` ); expect(res.status).toBe(400); expect(res.body.message).toBe(MONGO_OBJ_ID_MALFORMED_MESSAGE); diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 7a5e38c460..cadaa8095c 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -81,7 +81,6 @@ const CollabSandbox: React.FC = () => { return; } getQuestionById(questionId, dispatch); - setCompilerResult([]); resetCollab(); diff --git a/frontend/src/pages/Profile/index.tsx b/frontend/src/pages/Profile/index.tsx index c942af986d..19a1f7e0d1 100644 --- a/frontend/src/pages/Profile/index.tsx +++ b/frontend/src/pages/Profile/index.tsx @@ -1,7 +1,7 @@ import { useNavigate, useParams } from "react-router-dom"; import AppMargin from "../../components/AppMargin"; import ProfileDetails from "../../components/ProfileDetails"; -import { Box, Button, Divider, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, Typography } from "@mui/material"; +import { Autocomplete, Box, Button, Divider, Grid2, IconButton, InputAdornment, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Typography } from "@mui/material"; import classes from "./index.module.css"; import { useEffect, useReducer, useState } from "react"; import { useAuth } from "../../contexts/AuthContext"; @@ -17,11 +17,19 @@ import qnHistoryReducer, { getQnHistoryList, initialQHState } from "../../reduce import { grey } from "@mui/material/colors"; import { convertDateString } from "../../utils/sessionTime"; import Loader from "../../components/Loader"; +import { Search } from "@mui/icons-material"; +import useDebounce from "../../utils/debounce"; const rowsPerPage = 10; +const searchCharacterLimit = 100; +const statusList = ["Accepted", "Attempted", "Rejected"]; const ProfilePage: React.FC = () => { const [page, setPage] = useState(0); + const [sortOrder, setSortOrder] = useState(1); + const [searchInput, setSearchInput] = useState(""); + const [searchFilter, setSearchFilter] = useDebounce("", 1000); + const [statusFilter, setStatusFilter] = useDebounce([], 1000); const [state, dispatch] = useReducer(qnHistoryReducer, initialQHState); const [loading, setLoading] = useState(true); const navigate = useNavigate(); @@ -66,14 +74,30 @@ const ProfilePage: React.FC = () => { page + 1, // convert from 0-based indexing rowsPerPage, userId, + searchFilter, + statusFilter, + sortOrder, dispatch ); } }; + useEffect(() => { + if (page !== 0) { + setPage(0); + } else { + updateQnHistoryList(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchFilter, statusFilter, sortOrder]); + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => updateQnHistoryList(), [page]); + const areQnHistoriesFiltered = () => { + return (searchFilter || statusFilter.length > 0); + }; + if (loading) { return ; } @@ -89,7 +113,7 @@ const ProfilePage: React.FC = () => { const isCurrentUser = auth.user?.id === userId; - const tableHeaders = ["Title", "Status", "Date submitted"]; + const tableHeaders = ["Title", "Status", "Date attempted"]; return ( @@ -140,7 +164,79 @@ const ProfilePage: React.FC = () => { ({ flex: 3, paddingLeft: theme.spacing(4) })}> - Questions attempted + + Questions attempted + {state.qnHistories.length !== 0 && + } + + ({ + marginTop: theme.spacing(2), + "& .MuiTextField-root": { width: "100%" }, + })} + > + + + + + + + ), + }, + htmlInput: { + maxLength: searchCharacterLimit, + }, + formHelperText: { + sx: { textAlign: "right" }, + }, + }} + label="Title" + onChange={(input) => { + setSearchInput(input.target.value); + setSearchFilter(input.target.value.toLowerCase().trim()); + }} + helperText={ + searchInput.length + ` / ${searchCharacterLimit} characters` + } + disabled={state.qnHistories.length === 0 && !areQnHistoriesFiltered()} + /> + + + { + setStatusFilter(selectedOptions); + }} + renderInput={(params) => ( + + )} + disabled={state.qnHistories.length === 0 && !areQnHistoriesFiltered()} + /> + + ({ @@ -216,6 +312,7 @@ const ProfilePage: React.FC = () => { > {convertDateString(qnHistory.dateAttempted)} diff --git a/frontend/src/pages/QuestionHistoryDetail/index.tsx b/frontend/src/pages/QuestionHistoryDetail/index.tsx index ed4ad658dd..e94c9cd994 100644 --- a/frontend/src/pages/QuestionHistoryDetail/index.tsx +++ b/frontend/src/pages/QuestionHistoryDetail/index.tsx @@ -52,7 +52,7 @@ const QuestionHistoryDetail: React.FC = () => { const { user } = auth; - const tableHeaders = ["Status", "Date submitted", "Time taken", "Partner"]; + const tableHeaders = ["Status", "Date attempted", "Time taken", "Partner"]; useEffect(() => { if (!qnHistoryId) { @@ -113,7 +113,7 @@ const QuestionHistoryDetail: React.FC = () => { - Latest submission details + Latest attempt details {user && qnhistState.selectedQnHistory && ( @@ -162,7 +162,7 @@ const QuestionHistoryDetail: React.FC = () => { borderRight: "1px solid #E0E0E0", }} > - + {convertDateString( qnhistState.selectedQnHistory.dateAttempted )} diff --git a/frontend/src/reducers/qnHistoryReducer.ts b/frontend/src/reducers/qnHistoryReducer.ts index b480068e85..7a89ecd7e0 100644 --- a/frontend/src/reducers/qnHistoryReducer.ts +++ b/frontend/src/reducers/qnHistoryReducer.ts @@ -2,6 +2,19 @@ import { Dispatch } from "react"; import { qnHistoryClient } from "../utils/api"; import { isString, isStringArray } from "../utils/typeChecker"; +/*type CompilerResult = { + status?: string; + exception?: string; + stdout?: string; + stderr?: string; + executionTime?: number; + stdin: string; + stout?: string; + actualResult?: string; + expectedResult: string; + isMatch?: boolean; +};*/ + type QnHistoryDetail = { id: string; userIds: Array; @@ -12,6 +25,7 @@ type QnHistoryDetail = { timeTaken: number; code: string; language: string; + //compilerRes: CompilerResult; }; type QnHistoryList = { @@ -92,7 +106,7 @@ export const initialQHState: QnHistoriesState = { }; export const createQnHistory = async ( - qnHistory: Omit, + qnHistory: Omit, dispatch: Dispatch ): Promise => { return qnHistoryClient @@ -103,7 +117,9 @@ export const createQnHistory = async ( submissionStatus: qnHistory.submissionStatus, dateAttempted: qnHistory.dateAttempted, timeTaken: qnHistory.timeTaken, + code: qnHistory.code, language: qnHistory.language, + //compilerRes: qnHistory.compilerRes, }) .then((res) => { dispatch({ @@ -125,6 +141,9 @@ export const getQnHistoryList = ( page: number, qnHistLimit: number, userId: string, + title: string, + status: string[], + order: number, dispatch: Dispatch ) => { qnHistoryClient @@ -133,6 +152,9 @@ export const getQnHistoryList = ( page: page, qnHistLimit: qnHistLimit, userId: userId, + title: title, + status: status, + order: order, }, }) .then((res) => { diff --git a/frontend/src/utils/sessionTime.ts b/frontend/src/utils/sessionTime.ts index 625ef18254..a769d2d9ca 100644 --- a/frontend/src/utils/sessionTime.ts +++ b/frontend/src/utils/sessionTime.ts @@ -8,9 +8,17 @@ export const extractSecondsFromTime = (time: number) => time % 60; // after extr export const extractMinutesOnly = (time: number) => time / 60; export const convertDateString = (date: string): string => { - return new Date(date).toLocaleDateString("en-GB", { + const convertedDate = new Date(date); + const dateString = convertedDate.toLocaleDateString("en-GB", { day: "2-digit", month: "2-digit", year: "numeric", }); + + const timeString = convertedDate.toLocaleTimeString("en-GB", { + hour: "2-digit", + minute: "2-digit", + }); + + return dateString + " " + timeString; }; From 70c3c6db0e106a0c951ea416b7c499ab3f961600 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Thu, 7 Nov 2024 18:22:53 +0800 Subject: [PATCH 129/192] Add token verification for socket connection + refactor code --- backend/collab-service/.env.sample | 3 ++ .../src/api/userService.ts} | 0 .../src/middlewares/basicAccessControl.ts | 19 +++++++++ backend/collab-service/src/server.ts | 4 ++ .../src/api/userService.ts | 15 +++++++ .../src/middlewares/basicAccessControl.ts | 19 +++++++++ backend/communication-service/src/server.ts | 16 +------- backend/matching-service/.env.sample | 1 + .../src/api/questionHistoryService.ts | 31 ++++++++++++++ .../src/api/questionService.ts | 16 ++++++++ .../matching-service/src/api/userService.ts | 15 +++++++ .../matching-service/src/config/rabbitmq.ts | 2 +- .../src/handlers/websocketHandler.ts | 40 +++++++++---------- .../src/middlewares/basicAccessControl.ts | 19 +++++++++ backend/matching-service/src/server.ts | 5 +++ backend/matching-service/src/utils/api.ts | 23 ----------- .../utils/{mq_utils.ts => messageQueue.ts} | 0 frontend/src/utils/collabSocket.ts | 4 ++ frontend/src/utils/matchSocket.ts | 3 ++ 19 files changed, 175 insertions(+), 60 deletions(-) rename backend/{communication-service/src/utils/userServiceApi.ts => collab-service/src/api/userService.ts} (100%) create mode 100644 backend/collab-service/src/middlewares/basicAccessControl.ts create mode 100644 backend/communication-service/src/api/userService.ts create mode 100644 backend/communication-service/src/middlewares/basicAccessControl.ts create mode 100644 backend/matching-service/src/api/questionHistoryService.ts create mode 100644 backend/matching-service/src/api/questionService.ts create mode 100644 backend/matching-service/src/api/userService.ts create mode 100644 backend/matching-service/src/middlewares/basicAccessControl.ts delete mode 100644 backend/matching-service/src/utils/api.ts rename backend/matching-service/src/utils/{mq_utils.ts => messageQueue.ts} (100%) diff --git a/backend/collab-service/.env.sample b/backend/collab-service/.env.sample index 94020b37a8..65939ae031 100644 --- a/backend/collab-service/.env.sample +++ b/backend/collab-service/.env.sample @@ -4,6 +4,9 @@ SERVICE_PORT=3003 # Origins for cors ORIGINS=http://localhost:5173,http://127.0.0.1:5173 +# Other service APIs +USER_SERVICE_URL=http://user-service:3001/api + # Redis configuration REDIS_URI=redis://collab-service-redis:6379 diff --git a/backend/communication-service/src/utils/userServiceApi.ts b/backend/collab-service/src/api/userService.ts similarity index 100% rename from backend/communication-service/src/utils/userServiceApi.ts rename to backend/collab-service/src/api/userService.ts diff --git a/backend/collab-service/src/middlewares/basicAccessControl.ts b/backend/collab-service/src/middlewares/basicAccessControl.ts new file mode 100644 index 0000000000..727ee6783e --- /dev/null +++ b/backend/collab-service/src/middlewares/basicAccessControl.ts @@ -0,0 +1,19 @@ +import { ExtendedError, Socket } from "socket.io"; +import { verifyToken } from "../api/userService.ts"; + +export const verifyUserToken = ( + socket: Socket, + next: (err?: ExtendedError) => void +) => { + const token = + socket.handshake.headers.authorization || socket.handshake.auth.token; + verifyToken(token) + .then(() => { + console.log("Valid credentials"); + next(); + }) + .catch((err) => { + console.error(err); + next(new Error("Unauthorized")); + }); +}; diff --git a/backend/collab-service/src/server.ts b/backend/collab-service/src/server.ts index c1d11c7333..1a00c9c42c 100644 --- a/backend/collab-service/src/server.ts +++ b/backend/collab-service/src/server.ts @@ -3,8 +3,10 @@ import app, { allowedOrigins } from "./app.ts"; import { handleWebsocketCollabEvents } from "./handlers/websocketHandler.ts"; import { Server, Socket } from "socket.io"; import { connectRedis } from "./config/redis.ts"; +import { verifyUserToken } from "./middlewares/basicAccessControl.ts"; const server = http.createServer(app); + export const io = new Server(server, { cors: { origin: allowedOrigins, @@ -13,6 +15,8 @@ export const io = new Server(server, { connectionStateRecovery: {}, }); +io.use(verifyUserToken); + io.on("connection", (socket: Socket) => { handleWebsocketCollabEvents(socket); }); diff --git a/backend/communication-service/src/api/userService.ts b/backend/communication-service/src/api/userService.ts new file mode 100644 index 0000000000..88442a3b6c --- /dev/null +++ b/backend/communication-service/src/api/userService.ts @@ -0,0 +1,15 @@ +import axios from "axios"; + +const USER_SERVICE_URL = + process.env.USER_SERVICE_URL || "http://localhost:3001/api"; + +const userClient = axios.create({ + baseURL: USER_SERVICE_URL, + withCredentials: true, +}); + +export const verifyToken = (token: string | undefined) => { + return userClient.get("/auth/verify-token", { + headers: { authorization: token }, + }); +}; diff --git a/backend/communication-service/src/middlewares/basicAccessControl.ts b/backend/communication-service/src/middlewares/basicAccessControl.ts new file mode 100644 index 0000000000..15088e9a86 --- /dev/null +++ b/backend/communication-service/src/middlewares/basicAccessControl.ts @@ -0,0 +1,19 @@ +import { ExtendedError, Socket } from "socket.io"; +import { verifyToken } from "../api/userService"; + +export const verifyUserToken = ( + socket: Socket, + next: (err?: ExtendedError) => void +) => { + const token = + socket.handshake.headers.authorization || socket.handshake.auth.token; + verifyToken(token) + .then(() => { + console.log("Valid credentials"); + next(); + }) + .catch((err) => { + console.error(err); + next(new Error("Unauthorized")); + }); +}; diff --git a/backend/communication-service/src/server.ts b/backend/communication-service/src/server.ts index 4c2ebd4140..5421fe6f17 100644 --- a/backend/communication-service/src/server.ts +++ b/backend/communication-service/src/server.ts @@ -2,7 +2,7 @@ import app, { allowedOrigins } from "./app"; import { createServer } from "http"; import { Server } from "socket.io"; import { handleWebsocketCommunicationEvents } from "./handlers/websocketHandler"; -import { verifyToken } from "./utils/userServiceApi"; +import { verifyUserToken } from "./middlewares/basicAccessControl"; const PORT = process.env.SERVICE_PORT || 3005; @@ -13,19 +13,7 @@ export const io = new Server(server, { connectionStateRecovery: {}, }); -io.use((socket, next) => { - const token = - socket.handshake.headers.authorization || socket.handshake.auth.token; - verifyToken(token) - .then(() => { - console.log("Valid credentials"); - next(); - }) - .catch((err) => { - console.error(err); - next(new Error("Unauthorized")); - }); -}); +io.use(verifyUserToken); io.on("connection", handleWebsocketCommunicationEvents); diff --git a/backend/matching-service/.env.sample b/backend/matching-service/.env.sample index 83217a9e53..044f09d289 100644 --- a/backend/matching-service/.env.sample +++ b/backend/matching-service/.env.sample @@ -7,6 +7,7 @@ ORIGINS=http://localhost:5173,http://127.0.0.1:5173 # Other service APIs QUESTION_SERVICE_URL=http://question-service:3000/api/questions QN_HISTORY_SERVICE_URL=http://qn-history-service:3006/api/qnhistories +USER_SERVICE_URL=http://user-service:3001/api # RabbitMq configuration RABBITMQ_DEFAULT_USER=admin diff --git a/backend/matching-service/src/api/questionHistoryService.ts b/backend/matching-service/src/api/questionHistoryService.ts new file mode 100644 index 0000000000..0a17c16703 --- /dev/null +++ b/backend/matching-service/src/api/questionHistoryService.ts @@ -0,0 +1,31 @@ +import axios from "axios"; + +const QN_HISTORY_SERVICE_URL = + process.env.QN_HISTORY_SERVICE_URL || + "http://qn-history-service:3006/api/qnhistories"; + +const qnHistoryService = axios.create({ + baseURL: QN_HISTORY_SERVICE_URL, + headers: { + "Content-Type": "application/json", + }, +}); + +export const createQuestionHistory = ( + questionId: string, + title: string, + submissionStatus: string, + language: string, + ...userIds: string[] +) => { + const dateAttempted = new Date(); + return qnHistoryService.post("/", { + userIds, + questionId, + title, + submissionStatus, + language, + dateAttempted, + timeTaken: 0, + }); +}; diff --git a/backend/matching-service/src/api/questionService.ts b/backend/matching-service/src/api/questionService.ts new file mode 100644 index 0000000000..7599df3bc2 --- /dev/null +++ b/backend/matching-service/src/api/questionService.ts @@ -0,0 +1,16 @@ +import axios from "axios"; + +const QUESTION_SERVICE_URL = + process.env.QUESTION_SERVICE_URL || + "http://question-service:3000/api/questions"; + +const questionClient = axios.create({ + baseURL: QUESTION_SERVICE_URL, + headers: { + "Content-Type": "application/json", + }, +}); + +export const getRandomQuestion = (complexity: string, category: string) => { + return questionClient.get("/random", { params: { complexity, category } }); +}; diff --git a/backend/matching-service/src/api/userService.ts b/backend/matching-service/src/api/userService.ts new file mode 100644 index 0000000000..88442a3b6c --- /dev/null +++ b/backend/matching-service/src/api/userService.ts @@ -0,0 +1,15 @@ +import axios from "axios"; + +const USER_SERVICE_URL = + process.env.USER_SERVICE_URL || "http://localhost:3001/api"; + +const userClient = axios.create({ + baseURL: USER_SERVICE_URL, + withCredentials: true, +}); + +export const verifyToken = (token: string | undefined) => { + return userClient.get("/auth/verify-token", { + headers: { authorization: token }, + }); +}; diff --git a/backend/matching-service/src/config/rabbitmq.ts b/backend/matching-service/src/config/rabbitmq.ts index 966e98cbd6..0623f88b02 100644 --- a/backend/matching-service/src/config/rabbitmq.ts +++ b/backend/matching-service/src/config/rabbitmq.ts @@ -1,6 +1,6 @@ import amqplib, { Connection } from "amqplib"; import dotenv from "dotenv"; -import { matchUsers } from "../utils/mq_utils"; +import { matchUsers } from "../utils/messageQueue"; import { MatchRequestItem } from "../handlers/matchHandler"; import { Complexities, Categories, Languages } from "../utils/constants"; diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index a738530bfc..74cdd526e9 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -11,7 +11,8 @@ import { } from "./matchHandler"; import { io } from "../server"; import { v4 as uuidv4 } from "uuid"; -import { qnHistoryService, questionService } from "../utils/api"; +import { getRandomQuestion } from "../api/questionService"; +import { createQuestionHistory } from "../api/questionHistoryService"; enum MatchEvents { // Receive @@ -130,28 +131,23 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { } const { complexity, category, language } = match; - questionService - .get("/random", { params: { complexity, category } }) - .then((res) => { - const qnId = res.data.question.id; - qnHistoryService - .post("/", { - userIds: [userId1, userId2], - questionId: qnId, - title: res.data.question.title, - submissionStatus: "Attempted", - dateAttempted: new Date(), - timeTaken: 0, - language: language, - }) - .then((res) => { - io.to(matchId).emit( - MatchEvents.MATCH_SUCCESSFUL, - qnId, - res.data.qnHistory.id - ); - }); + getRandomQuestion(complexity, category).then((res) => { + const qnId = res.data.question.id; + createQuestionHistory( + qnId, + res.data.question.title, + "Attempted", + language, + userId1, + userId2 + ).then((res) => { + io.to(matchId).emit( + MatchEvents.MATCH_SUCCESSFUL, + qnId, + res.data.qnHistory.id + ); }); + }); } } ); diff --git a/backend/matching-service/src/middlewares/basicAccessControl.ts b/backend/matching-service/src/middlewares/basicAccessControl.ts new file mode 100644 index 0000000000..15088e9a86 --- /dev/null +++ b/backend/matching-service/src/middlewares/basicAccessControl.ts @@ -0,0 +1,19 @@ +import { ExtendedError, Socket } from "socket.io"; +import { verifyToken } from "../api/userService"; + +export const verifyUserToken = ( + socket: Socket, + next: (err?: ExtendedError) => void +) => { + const token = + socket.handshake.headers.authorization || socket.handshake.auth.token; + verifyToken(token) + .then(() => { + console.log("Valid credentials"); + next(); + }) + .catch((err) => { + console.error(err); + next(new Error("Unauthorized")); + }); +}; diff --git a/backend/matching-service/src/server.ts b/backend/matching-service/src/server.ts index fc627d78ca..0c420ce87a 100644 --- a/backend/matching-service/src/server.ts +++ b/backend/matching-service/src/server.ts @@ -3,8 +3,11 @@ import app, { allowedOrigins } from "./app.ts"; import { handleWebsocketMatchEvents } from "./handlers/websocketHandler.ts"; import { Server } from "socket.io"; import { connectToRabbitMq } from "./config/rabbitmq.ts"; +import { verifyToken } from "./api/userService.ts"; +import { verifyUserToken } from "./middlewares/basicAccessControl.ts"; const server = http.createServer(app); + export const io = new Server(server, { cors: { origin: allowedOrigins, @@ -13,6 +16,8 @@ export const io = new Server(server, { connectionStateRecovery: {}, }); +io.use(verifyUserToken); + io.on("connection", (socket) => { handleWebsocketMatchEvents(socket); }); diff --git a/backend/matching-service/src/utils/api.ts b/backend/matching-service/src/utils/api.ts deleted file mode 100644 index 1b088f4c05..0000000000 --- a/backend/matching-service/src/utils/api.ts +++ /dev/null @@ -1,23 +0,0 @@ -import axios from "axios"; - -const QUESTION_SERVICE_URL = - process.env.QUESTION_SERVICE_URL || - "http://question-service:3000/api/questions"; - -const QN_HISTORY_SERVICE_URL = - process.env.QN_HISTORY_SERVICE_URL || - "http://qn-history-service:3006/api/qnhistories"; - -export const questionService = axios.create({ - baseURL: QUESTION_SERVICE_URL, - headers: { - "Content-Type": "application/json", - }, -}); - -export const qnHistoryService = axios.create({ - baseURL: QN_HISTORY_SERVICE_URL, - headers: { - "Content-Type": "application/json", - }, -}); diff --git a/backend/matching-service/src/utils/mq_utils.ts b/backend/matching-service/src/utils/messageQueue.ts similarity index 100% rename from backend/matching-service/src/utils/mq_utils.ts rename to backend/matching-service/src/utils/messageQueue.ts diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 4fe50bafe3..1d252552cc 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -33,9 +33,13 @@ export type CollabSessionData = { }; const COLLAB_SOCKET_URL = "http://localhost:3003"; + export const collabSocket = io(COLLAB_SOCKET_URL, { reconnectionAttempts: 3, autoConnect: false, + auth: { + token: `Bearer ${localStorage.getItem("token")}`, + }, }); let doc: Doc; diff --git a/frontend/src/utils/matchSocket.ts b/frontend/src/utils/matchSocket.ts index e79801a6b4..4ef4d2038b 100644 --- a/frontend/src/utils/matchSocket.ts +++ b/frontend/src/utils/matchSocket.ts @@ -5,4 +5,7 @@ const MATCH_SOCKET_URL = "http://localhost:3002"; export const matchSocket = io(MATCH_SOCKET_URL, { reconnectionAttempts: 3, autoConnect: false, + auth: { + token: `Bearer ${localStorage.getItem("token")}`, + }, }); From bcc0d1316c874719c0ad8a0066b21f380c54ac13 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:24:07 +0800 Subject: [PATCH 130/192] change create qn history record to be at collab instead of match --- backend/collab-service/.env.sample | 2 + .../src/handlers/websocketHandler.ts | 56 +++++++++++++++---- backend/collab-service/src/utils/api.ts | 12 ++++ backend/matching-service/.env.sample | 1 - .../src/handlers/websocketHandler.ts | 30 ++++------ backend/matching-service/src/utils/api.ts | 11 ---- frontend/src/components/CodeEditor/index.tsx | 19 +++++-- frontend/src/contexts/CollabContext.tsx | 17 ++++-- frontend/src/contexts/MatchContext.tsx | 10 ++-- frontend/src/utils/collabSocket.ts | 22 +++++++- 10 files changed, 120 insertions(+), 60 deletions(-) create mode 100644 backend/collab-service/src/utils/api.ts diff --git a/backend/collab-service/.env.sample b/backend/collab-service/.env.sample index 94020b37a8..33c7141740 100644 --- a/backend/collab-service/.env.sample +++ b/backend/collab-service/.env.sample @@ -4,6 +4,8 @@ SERVICE_PORT=3003 # Origins for cors ORIGINS=http://localhost:5173,http://127.0.0.1:5173 +QN_HISTORY_SERVICE_URL=http://qn-history-service:3006/api/qnhistories + # Redis configuration REDIS_URI=redis://collab-service-redis:6379 diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index ee2948d436..38a3c1e982 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -2,6 +2,7 @@ import { Socket } from "socket.io"; import { io } from "../server"; import redisClient from "../config/redis"; import { Doc, applyUpdateV2, encodeStateAsUpdateV2 } from "yjs"; +import { qnHistoryService } from "../utils/api"; enum CollabEvents { // Receive @@ -55,19 +56,50 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { } }); - socket.on(CollabEvents.INIT_DOCUMENT, (roomId: string, template: string) => { - const doc = getDocument(roomId); - const isPartnerReady = partnerReadiness.get(roomId); - - if (isPartnerReady && doc.getText().length === 0) { - doc.transact(() => { - doc.getText().insert(0, template); - }); - io.to(roomId).emit(CollabEvents.DOCUMENT_READY); - } else { - partnerReadiness.set(roomId, true); + socket.on( + CollabEvents.INIT_DOCUMENT, + ( + roomId: string, + template: string, + uid1: string, + uid2: string, + language: string, + qnId: string, + qnTitle: string + ) => { + const doc = getDocument(roomId); + const isPartnerReady = partnerReadiness.get(roomId); + + if (isPartnerReady && doc.getText().length === 0) { + qnHistoryService + .post("/", { + userIds: [uid1, uid2], + questionId: qnId, + title: qnTitle, + submissionStatus: "Attempted", + dateAttempted: new Date(), + timeTaken: 0, + code: template, + language: language, + }) + .then((res) => { + console.log("created in collab"); + doc.transact(() => { + doc.getText().insert(0, template); + }); + io.to(roomId).emit( + CollabEvents.DOCUMENT_READY, + res.data.qnHistory.id + ); + }) + .catch((err) => { + console.log(err); + }); + } else { + partnerReadiness.set(roomId, true); + } } - }); + ); socket.on( CollabEvents.UPDATE_REQUEST, diff --git a/backend/collab-service/src/utils/api.ts b/backend/collab-service/src/utils/api.ts new file mode 100644 index 0000000000..987537be9b --- /dev/null +++ b/backend/collab-service/src/utils/api.ts @@ -0,0 +1,12 @@ +import axios from "axios"; + +const QN_HISTORY_SERVICE_URL = + process.env.QN_HISTORY_SERVICE_URL || + "http://qn-history-service:3006/api/qnhistories"; + +export const qnHistoryService = axios.create({ + baseURL: QN_HISTORY_SERVICE_URL, + headers: { + "Content-Type": "application/json", + }, +}); diff --git a/backend/matching-service/.env.sample b/backend/matching-service/.env.sample index 83217a9e53..506314e3c6 100644 --- a/backend/matching-service/.env.sample +++ b/backend/matching-service/.env.sample @@ -6,7 +6,6 @@ ORIGINS=http://localhost:5173,http://127.0.0.1:5173 # Other service APIs QUESTION_SERVICE_URL=http://question-service:3000/api/questions -QN_HISTORY_SERVICE_URL=http://qn-history-service:3006/api/qnhistories # RabbitMq configuration RABBITMQ_DEFAULT_USER=admin diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index a738530bfc..cbb58b029d 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -11,7 +11,7 @@ import { } from "./matchHandler"; import { io } from "../server"; import { v4 as uuidv4 } from "uuid"; -import { qnHistoryService, questionService } from "../utils/api"; +import { questionService } from "../utils/api"; enum MatchEvents { // Receive @@ -129,28 +129,18 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { return; } - const { complexity, category, language } = match; + const { complexity, category } = match; questionService .get("/random", { params: { complexity, category } }) .then((res) => { - const qnId = res.data.question.id; - qnHistoryService - .post("/", { - userIds: [userId1, userId2], - questionId: qnId, - title: res.data.question.title, - submissionStatus: "Attempted", - dateAttempted: new Date(), - timeTaken: 0, - language: language, - }) - .then((res) => { - io.to(matchId).emit( - MatchEvents.MATCH_SUCCESSFUL, - qnId, - res.data.qnHistory.id - ); - }); + io.to(matchId).emit( + MatchEvents.MATCH_SUCCESSFUL, + res.data.question.id, + res.data.question.title + ); + }) + .catch((err) => { + console.log(err); }); } } diff --git a/backend/matching-service/src/utils/api.ts b/backend/matching-service/src/utils/api.ts index 1b088f4c05..76c414befc 100644 --- a/backend/matching-service/src/utils/api.ts +++ b/backend/matching-service/src/utils/api.ts @@ -4,20 +4,9 @@ const QUESTION_SERVICE_URL = process.env.QUESTION_SERVICE_URL || "http://question-service:3000/api/questions"; -const QN_HISTORY_SERVICE_URL = - process.env.QN_HISTORY_SERVICE_URL || - "http://qn-history-service:3006/api/qnhistories"; - export const questionService = axios.create({ baseURL: QUESTION_SERVICE_URL, headers: { "Content-Type": "application/json", }, }); - -export const qnHistoryService = axios.create({ - baseURL: QN_HISTORY_SERVICE_URL, - headers: { - "Content-Type": "application/json", - }, -}); diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 469c2c927a..d8df708837 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -11,7 +11,8 @@ import { yCollab } from "y-codemirror.next"; import { Text } from "yjs"; import { Awareness } from "y-protocols/awareness"; import { useCollab } from "../../contexts/CollabContext"; -import { USE_COLLAB_ERROR_MESSAGE } from "../../utils/constants"; +import { USE_COLLAB_ERROR_MESSAGE, USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; +import { useMatch } from "../../contexts/MatchContext"; interface CodeEditorProps { editorState?: { text: Text; awareness: Awareness }; @@ -40,12 +41,19 @@ const CodeEditor: React.FC = (props) => { isReadOnly = false, } = props; + const match = useMatch(); + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + + const { matchCriteria, matchUser, partner, questionId, questionTitle } = match; + const collab = useCollab(); if (!collab) { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { setCode } = collab; + const { setCode, checkDocReady } = collab; const [isEditorReady, setIsEditorReady] = useState(false); const [isDocumentLoaded, setIsDocumentLoaded] = useState(false); @@ -66,8 +74,11 @@ const CodeEditor: React.FC = (props) => { } const loadTemplate = async () => { - await initDocument(uid, roomId, template); - setIsDocumentLoaded(true); + if (matchUser && partner && matchCriteria && questionId && questionTitle) { + await initDocument(uid, roomId, template, matchUser.id, partner.id, matchCriteria.language, questionId, questionTitle); + checkDocReady(); + setIsDocumentLoaded(true); + } }; loadTemplate(); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 622d42e857..2ebb2fdaa2 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -44,6 +44,7 @@ type CollabContextType = { isEndSessionModalOpen: boolean; time: number; resetCollab: () => void; + checkDocReady: () => void; }; const CollabContext = createContext(null); @@ -65,7 +66,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { getMatchId, stopMatch, questionId, - qnHistoryId, } = match; const [time, setTime] = useState(0); @@ -88,6 +88,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [compilerResult, setCompilerResult] = useState([]); const [isEndSessionModalOpen, setIsEndSessionModalOpen] = useState(false); + const [qnHistoryId, setQnHistoryId] = useState(null); + let hasSubmitted: boolean = false; const handleSubmitSessionClick = async () => { try { @@ -97,6 +99,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { code: code.replace(/\t/g, " ".repeat(4)), language: matchCriteria?.language.toLowerCase(), }); + hasSubmitted = true; console.log([...res.data.data]); setCompilerResult([...res.data.data]); @@ -140,11 +143,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const handleConfirmEndSession = async () => { setIsEndSessionModalOpen(false); - // Get queston history - const data = await qnHistoryClient.get(qnHistoryId as string); - // Only update question history if it has not been submitted before - if (!data.data.qnHistory.code) { + if (!hasSubmitted) { updateQnHistoryById( qnHistoryId as string, { @@ -172,6 +172,12 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { resetCollab(); }; + const checkDocReady = () => { + collabSocket.on(CollabEvents.DOCUMENT_READY, (qnHistoryId: string) => { + setQnHistoryId(qnHistoryId); + }); + }; + const checkPartnerStatus = () => { collabSocket.on(CollabEvents.PARTNER_LEFT, () => { toast.error(COLLAB_ENDED_MESSAGE); @@ -200,6 +206,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { isEndSessionModalOpen, time, resetCollab, + checkDocReady, }} > {children} diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 4ac0b64308..27834e2f15 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -89,7 +89,7 @@ type MatchContextType = { matchPending: boolean; loading: boolean; questionId: string | null; - qnHistoryId: string | null; + questionTitle: string | null; }; const requestTimeoutDuration = 5000; @@ -115,7 +115,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [matchPending, setMatchPending] = useState(false); const [loading, setLoading] = useState(true); const [questionId, setQuestionId] = useState(null); - const [qnHistoryId, setQnHistoryId] = useState(null); + const [questionTitle, setQuestionTitle] = useState(null); const navigator = useContext(UNSAFE_NavigationContext).navigator as History; @@ -277,10 +277,10 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const initMatchedListeners = () => { matchSocket.on( MatchEvents.MATCH_SUCCESSFUL, - (qnId: string, qnHistId: string) => { + (qnId: string, title: string) => { setMatchPending(false); setQuestionId(qnId); - setQnHistoryId(qnHistId); + setQuestionTitle(title); appNavigate(MatchPaths.COLLAB); } ); @@ -512,7 +512,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { matchPending, loading, questionId, - qnHistoryId, + questionTitle, }} > {children} diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 4fe50bafe3..e0aa42be07 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -72,8 +72,26 @@ export const join = ( }); }; -export const initDocument = (uid: string, roomId: string, template: string) => { - collabSocket.emit(CollabEvents.INIT_DOCUMENT, roomId, template); +export const initDocument = ( + uid: string, + roomId: string, + template: string, + uid1: string, + uid2: string, + language: string, + qnId: string, + qnTitle: string +) => { + collabSocket.emit( + CollabEvents.INIT_DOCUMENT, + roomId, + template, + uid1, + uid2, + language, + qnId, + qnTitle + ); return new Promise((resolve) => { collabSocket.once(CollabEvents.UPDATE, (update) => { From 38cc60859d0a0f33dca43a6b1dae11c3dc75569c Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Thu, 7 Nov 2024 18:34:54 +0800 Subject: [PATCH 131/192] Modify alert message for collab page --- frontend/src/contexts/MatchContext.tsx | 74 +++++++--------------- frontend/src/pages/CollabSandbox/index.tsx | 35 ++++------ frontend/src/utils/constants.ts | 10 ++- 3 files changed, 43 insertions(+), 76 deletions(-) diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index c9f78dcfed..c699bec693 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -3,8 +3,10 @@ import React, { createContext, useContext, useEffect, useState } from "react"; import { matchSocket } from "../utils/matchSocket"; import { + ABORT_COLLAB_SESSION_CONFIRMATION_MESSAGE, ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE, FAILED_MATCH_REQUEST_MESSAGE, + MATCH_ACCEPTANCE_ERROR, MATCH_CONNECTION_ERROR, MATCH_LOGIN_REQUIRED_MESSAGE, MATCH_REQUEST_EXISTS_MESSAGE, @@ -17,9 +19,6 @@ import useAppNavigate from "../hooks/useAppNavigate"; import { UNSAFE_NavigationContext } from "react-router-dom"; import { Action, type History, type Transition } from "history"; -let matchUserId: string; -let partnerUserId: string; - type MatchUser = { id: string; username: string; @@ -81,7 +80,6 @@ type MatchContextType = { retryMatch: () => void; matchingTimeout: () => void; matchOfferTimeout: () => void; - verifyMatchStatus: () => void; getMatchId: () => string | null; matchUser: MatchUser | null; matchCriteria: MatchCriteria | null; @@ -126,20 +124,17 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { username: user.username, profile: user.profilePictureUrl, }); - matchUserId = user.id; } else { setMatchUser(null); - matchUserId = ""; } }, [user]); useEffect(() => { - if ( - !matchUser?.id || - (location.pathname !== MatchPaths.MATCHING && - location.pathname !== MatchPaths.MATCHED && - location.pathname !== MatchPaths.COLLAB) - ) { + const isMatchPage = + location.pathname === MatchPaths.MATCHING || + location.pathname === MatchPaths.MATCHED; + const isCollabPage = location.pathname == MatchPaths.COLLAB; + if (!matchUser?.id || !(isMatchPage || isCollabPage)) { resetMatchStates(); return; } @@ -147,21 +142,25 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { openSocketConnection(); matchSocket.emit(MatchEvents.USER_CONNECTED, matchUser?.id); + const message = isMatchPage + ? ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE + : ABORT_COLLAB_SESSION_CONFIRMATION_MESSAGE; + + // handle page leave (navigate away) const unblock = navigator.block((transition: Transition) => { - if ( - transition.action === Action.Replace || - confirm(ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE) - ) { + if (transition.action === Action.Replace || confirm(message)) { unblock(); appNavigate(transition.location.pathname); } }); + // handle tab closure / url change const handleBeforeUnload = (e: BeforeUnloadEvent) => { e.preventDefault(); - e.returnValue = ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE; // for legacy support, does not actually display message + e.returnValue = message; // for legacy support, does not actually display message }; + // handle page refresh / tab closure const handleUnload = () => { closeSocketConnection(); }; @@ -171,6 +170,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { return () => { closeSocketConnection(); + unblock(); window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("unload", handleUnload); }; @@ -183,7 +183,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } setMatchId(null); setPartner(null); - partnerUserId = ""; setMatchPending(false); setLoading(false); }; @@ -307,10 +306,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setMatchId(matchId); if (matchUser?.id === user1.id) { setPartner(user2); - partnerUserId = user2.id; } else { setPartner(user1); - partnerUserId = user1.id; } setMatchPending(true); appNavigate(MatchPaths.MATCHED); @@ -389,11 +386,16 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const acceptMatch = () => { + if (!matchUser || !partner) { + toast.error(MATCH_ACCEPTANCE_ERROR); + return; + } + matchSocket.emit( MatchEvents.MATCH_ACCEPT_REQUEST, matchId, - matchUserId, - partnerUserId + matchUser.id, + partner.id ); }; @@ -429,7 +431,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { if (requested) { appNavigate(MatchPaths.MATCHING); setPartner(null); - partnerUserId = ""; } } ); @@ -464,32 +465,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { appNavigate(MatchPaths.HOME); }; - const verifyMatchStatus = () => { - const requestTimeout = setTimeout(() => { - setLoading(false); - toast.error(MATCH_CONNECTION_ERROR); - }, requestTimeoutDuration); - - setLoading(true); - matchSocket.emit( - MatchEvents.MATCH_STATUS_REQUEST, - matchUser?.id, - (match: { matchId: string; partner: MatchUser } | null) => { - clearTimeout(requestTimeout); - if (match) { - setMatchId(match.matchId); - setPartner(match.partner); - partnerUserId = match.partner.id; - } else { - setMatchId(null); - setPartner(null); - partnerUserId = ""; - } - setLoading(false); - } - ); - }; - const getMatchId = () => { return matchId; }; @@ -504,7 +479,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { retryMatch, matchingTimeout, matchOfferTimeout, - verifyMatchStatus, getMatchId, matchUser, matchCriteria, diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 7c1da0955d..d10184abd1 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -35,24 +35,12 @@ import { CollabSessionData, join, leave } from "../../utils/collabSocket"; import { toast } from "react-toastify"; const CollabSandbox: React.FC = () => { - const [editorState, setEditorState] = useState( - null - ); - const [isConnecting, setIsConnecting] = useState(true); - const match = useMatch(); if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { - // verifyMatchStatus, - getMatchId, - matchUser, - matchCriteria, - loading, - questionId, - } = match; + const { getMatchId, matchUser, matchCriteria, questionId } = match; const collab = useCollab(); if (!collab) { @@ -72,11 +60,13 @@ const CollabSandbox: React.FC = () => { const { selectedQuestion } = state; const [selectedTab, setSelectedTab] = useState<"tests" | "chat">("tests"); const [selectedTestcase, setSelectedTestcase] = useState(0); + const [editorState, setEditorState] = useState( + null + ); + const [isConnecting, setIsConnecting] = useState(true); + const matchId = getMatchId(); useEffect(() => { - // TODO: Retain session on page refresh - // verifyMatchStatus(); - if (!questionId) { return; } @@ -84,7 +74,6 @@ const CollabSandbox: React.FC = () => { resetCollab(); - const matchId = getMatchId(); if (!matchUser || !matchId) { return; } @@ -108,7 +97,9 @@ const CollabSandbox: React.FC = () => { connectToCollabSession(); // handle page refresh / tab closure - const handleUnload = () => leave(matchUser.id, matchId); + const handleUnload = () => { + leave(matchUser.id, matchId); + }; window.addEventListener("unload", handleUnload); return () => { @@ -119,11 +110,7 @@ const CollabSandbox: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - if (loading) { - return ; - } - - if (!matchUser || !matchCriteria || !getMatchId() || !isConnecting) { + if (!matchUser || !matchCriteria || !matchId || !isConnecting) { return ; } @@ -219,7 +206,7 @@ const CollabSandbox: React.FC = () => { ? selectedQuestion.cTemplate : "" } - roomId={getMatchId()!} + roomId={matchId} /> Date: Thu, 7 Nov 2024 18:36:35 +0800 Subject: [PATCH 132/192] add qn history tests and fix bug --- .../controllers/questionHistoryController.ts | 3 ++- .../tests/qnHistoryRoutes.spec.ts | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/backend/qn-history-service/src/controllers/questionHistoryController.ts b/backend/qn-history-service/src/controllers/questionHistoryController.ts index eaf6294c76..ad070f6615 100644 --- a/backend/qn-history-service/src/controllers/questionHistoryController.ts +++ b/backend/qn-history-service/src/controllers/questionHistoryController.ts @@ -142,8 +142,9 @@ export const readQnHistoryList = async ( return; } - if (!(orderInt == 1 || orderInt == -1)) { + if (orderInt !== 1 && orderInt !== -1) { res.status(400).json({ message: ORDER_INCORRECT_FORMAT_MESSAGE }); + return; } if (!userId.match(MONGO_OBJ_ID_FORMAT)) { diff --git a/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts b/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts index 0835e1a937..82245f8281 100644 --- a/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts +++ b/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts @@ -3,6 +3,7 @@ import supertest from "supertest"; import app from "../src/app"; import { MONGO_OBJ_ID_MALFORMED_MESSAGE, + ORDER_INCORRECT_FORMAT_MESSAGE, PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE, PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE, QN_HIST_NOT_FOUND_MESSAGE, @@ -24,6 +25,7 @@ describe("Qn History Routes", () => { const submissionStatus = "Attempted"; const dateAttempted = new Date(); const timeTaken = 0; + const code = "hi"; const language = "Python"; const newQnHistory = { userIds, @@ -32,6 +34,7 @@ describe("Qn History Routes", () => { submissionStatus, dateAttempted, timeTaken, + code, language, }; @@ -46,6 +49,7 @@ describe("Qn History Routes", () => { dateAttempted.toISOString() ); expect(res.body.qnHistory.timeTaken).toBe(timeTaken); + expect(res.body.qnHistory.code).toBe(code); expect(res.body.qnHistory.language).toBe(language); }); }); @@ -84,6 +88,14 @@ describe("Qn History Routes", () => { expect(res.body.message).toBe(PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE); }); + it("Does not read without order", async () => { + const res = await request.get( + `${BASE_URL}?page=1&qnHistLimit=10&userId=66f77e9f27ab3f794bdae664` + ); + expect(res.status).toBe(400); + expect(res.body.message).toBe(PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE); + }); + it("Does not read with negative page", async () => { const res = await request.get( `${BASE_URL}?page=-1&qnHistLimit=10&userId=66f77e9f27ab3f794bdae664&order=1` @@ -107,6 +119,14 @@ describe("Qn History Routes", () => { expect(res.status).toBe(400); expect(res.body.message).toBe(MONGO_OBJ_ID_MALFORMED_MESSAGE); }); + + it("Does not read with invalid order", async () => { + const res = await request.get( + `${BASE_URL}?page=1&qnHistLimit=10&userId=66f77e9f27ab3f794bdae664&order=2` + ); + expect(res.status).toBe(400); + expect(res.body.message).toBe(ORDER_INCORRECT_FORMAT_MESSAGE); + }); }); describe("GET /:id", () => { From 5a6c04fa383e10e71cafe673a93c20c4d0bea5b9 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Thu, 7 Nov 2024 20:28:40 +0800 Subject: [PATCH 133/192] Handle partner ending or leaving session --- .../src/handlers/websocketHandler.ts | 27 ++++--- .../src/components/CustomDialog/index.tsx | 75 +++++++++++++++++++ frontend/src/contexts/CollabContext.tsx | 26 +++++-- frontend/src/pages/CollabSandbox/index.tsx | 73 ++++-------------- frontend/src/utils/collabSocket.ts | 9 ++- frontend/src/utils/constants.ts | 6 +- 6 files changed, 140 insertions(+), 76 deletions(-) create mode 100644 frontend/src/components/CustomDialog/index.tsx diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index ee2948d436..184b6a890a 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -15,14 +15,14 @@ enum CollabEvents { // Send ROOM_READY = "room_ready", - DOCUMENT_READY = "document_ready", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", PARTNER_LEFT = "partner_left", + PARTNER_DISCONNECTED = "partner_disconnected", } const EXPIRY_TIME = 3600; -const CONNECTION_DELAY = 3000; // time window to allow for page re-renders / refresh +const CONNECTION_DELAY = 3000; // time window to allow for page re-renders const userConnections = new Map(); const collabSessions = new Map(); @@ -63,7 +63,6 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { doc.transact(() => { doc.getText().insert(0, template); }); - io.to(roomId).emit(CollabEvents.DOCUMENT_READY); } else { partnerReadiness.set(roomId, true); } @@ -93,17 +92,17 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { socket.on( CollabEvents.LEAVE, - (uid: string, roomId: string, isImmediate: boolean) => { + (uid: string, roomId: string, isIntentional: boolean) => { const connectionKey = `${uid}:${roomId}`; - if (isImmediate || !userConnections.has(connectionKey)) { - handleUserLeave(uid, roomId, socket); + if (isIntentional || !userConnections.has(connectionKey)) { + handleUserLeave(uid, roomId, socket, isIntentional); return; } clearTimeout(userConnections.get(connectionKey)!); const connectionTimeout = setTimeout(() => { - handleUserLeave(uid, roomId, socket); + handleUserLeave(uid, roomId, socket, isIntentional); }, CONNECTION_DELAY); userConnections.set(connectionKey, connectionTimeout); @@ -141,6 +140,7 @@ const removeCollabSession = (roomId: string) => { collabSessions.get(roomId)?.destroy(); collabSessions.delete(roomId); partnerReadiness.delete(roomId); + redisClient.del(roomId); }; const getDocument = (roomId: string) => { @@ -165,7 +165,12 @@ const saveDocument = async (roomId: string, doc: Doc) => { }); }; -const handleUserLeave = (uid: string, roomId: string, socket: Socket) => { +const handleUserLeave = ( + uid: string, + roomId: string, + socket: Socket, + isIntentional: boolean +) => { const connectionKey = `${uid}:${roomId}`; if (userConnections.has(connectionKey)) { clearTimeout(userConnections.get(connectionKey)!); @@ -179,6 +184,10 @@ const handleUserLeave = (uid: string, roomId: string, socket: Socket) => { if (!room || room.size === 0) { removeCollabSession(roomId); } else { - io.to(roomId).emit(CollabEvents.PARTNER_LEFT); + io.to(roomId).emit( + isIntentional + ? CollabEvents.PARTNER_LEFT + : CollabEvents.PARTNER_DISCONNECTED + ); } }; diff --git a/frontend/src/components/CustomDialog/index.tsx b/frontend/src/components/CustomDialog/index.tsx new file mode 100644 index 0000000000..611f296e62 --- /dev/null +++ b/frontend/src/components/CustomDialog/index.tsx @@ -0,0 +1,75 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from "@mui/material"; + +type CustomDialogProps = { + titleText: string; + bodyText: React.ReactNode; + primaryAction: string; + handlePrimaryAction: () => void; + secondaryAction: string; + open: boolean; + handleClose: () => void; +}; + +const CustomDialog: React.FC = (props) => { + const { + titleText, + bodyText, + primaryAction, + handlePrimaryAction, + secondaryAction, + open, + handleClose, + } = props; + + return ( + ({ + "& .MuiDialog-paper": { + padding: theme.spacing(2.5), + }, + })} + open={open} + onClose={handleClose} + > + + {titleText} + + + + {bodyText} + + + ({ + justifyContent: "center", + paddingBottom: theme.spacing(2.5), + })} + > + + + + + ); +}; + +export default CustomDialog; diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 052eed02b5..00a4a3b657 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -6,7 +6,9 @@ import { FAILED_TESTCASE_MESSAGE, SUCCESS_TESTCASE_MESSAGE, FAILED_TO_SUBMIT_CODE_MESSAGE, + COLLAB_PARTNER_DISCONNECTED_MESSAGE, COLLAB_ENDED_MESSAGE, + COLLAB_END_ERROR, } from "../utils/constants"; import { toast } from "react-toastify"; @@ -63,7 +65,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const { matchUser, - partner, matchCriteria, getMatchId, stopMatch, @@ -143,8 +144,14 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const handleConfirmEndSession = async () => { setIsEndSessionModalOpen(false); - // Get queston history - const data = await qnHistoryClient.get(qnHistoryId as string); + const roomId = getMatchId(); + if (!matchUser || !roomId || !qnHistoryId) { + toast.error(COLLAB_END_ERROR); + return; + } + + // Get question history + const data = await qnHistoryClient.get(qnHistoryId); // Only update question history if it has not been submitted before if (!data.data.qnHistory.code) { @@ -161,8 +168,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } // Leave collaboration room - leave(matchUser?.id as string, getMatchId() as string, true); - leave(partner?.id as string, getMatchId() as string, true); + leave(matchUser.id as string, roomId, true); + // TODO: partner leave // Leave chat room communicationSocket.emit(CommunicationEvents.USER_DISCONNECT); @@ -177,7 +184,14 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const checkPartnerStatus = () => { collabSocket.on(CollabEvents.PARTNER_LEFT, () => { - toast.error(COLLAB_ENDED_MESSAGE); + toast.info(COLLAB_ENDED_MESSAGE); + setIsEndSessionModalOpen(false); + stopMatch(); + appNavigate("/home"); + }); + + collabSocket.on(CollabEvents.PARTNER_DISCONNECTED, () => { + toast.error(COLLAB_PARTNER_DISCONNECTED_MESSAGE); setIsEndSessionModalOpen(false); stopMatch(); appNavigate("/home"); diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index d10184abd1..852043fe26 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -1,16 +1,5 @@ import AppMargin from "../../components/AppMargin"; -import { - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Grid2, - Tab, - Tabs, -} from "@mui/material"; +import { Box, Button, Grid2, Tab, Tabs } from "@mui/material"; import classes from "./index.module.css"; import { CompilerResult, useCollab } from "../../contexts/CollabContext"; import { useMatch } from "../../contexts/MatchContext"; @@ -33,6 +22,7 @@ import TestCase from "../../components/TestCase"; import CodeEditor from "../../components/CodeEditor"; import { CollabSessionData, join, leave } from "../../utils/collabSocket"; import { toast } from "react-toastify"; +import CustomDialog from "../../components/CustomDialog"; const CollabSandbox: React.FC = () => { const match = useMatch(); @@ -120,52 +110,21 @@ const CollabSandbox: React.FC = () => { return ( - - - {"End Session?"} - - - - Are you sure you want to end session? + + Are you sure you want to end the collaboration session?
- You will lose your current progress. -
-
- - - - -
+ You will not be able to rejoin. + + } + primaryAction="Confirm" + handlePrimaryAction={handleConfirmEndSession} + secondaryAction="Cancel" + open={isEndSessionModalOpen} + handleClose={handleRejectEndSession} + /> { }); }; -export const leave = (uid: string, roomId: string, isImmediate?: boolean) => { +export const leave = (uid: string, roomId: string, isIntentional?: boolean) => { collabSocket.removeAllListeners(); collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_SUCCESS); collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_FAILED); - collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isImmediate); - doc.destroy(); + collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isIntentional); + if (doc) { + doc.destroy(); + } }; export const sendCursorUpdate = (roomId: string, cursor: Cursor) => { diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 8e7fb44f64..715c3ef3be 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -104,9 +104,13 @@ export const QUESTION_DOES_NOT_EXIST_ERROR = // Collab export const COLLAB_ENDED_MESSAGE = - "Your partner has left the collaboration session."; + "Your partner has ended the collaboration session. The session will be closing soon..."; +export const COLLAB_PARTNER_DISCONNECTED_MESSAGE = + "Your partner has disconnected! The session will be closing soon..."; export const COLLAB_CONNECTION_ERROR = "Error connecting you to the collaboration session! Please try again."; +export const COLLAB_END_ERROR = + "Error ending the collaboration session! Please try again."; // Code execution export const FAILED_TESTCASE_MESSAGE = From b23898399a963f674f304c6decaa9f7a16d19f42 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Thu, 7 Nov 2024 21:01:19 +0800 Subject: [PATCH 134/192] Refactor code --- frontend/src/utils/collabSocket.ts | 3 ++- frontend/src/utils/communicationSocket.ts | 3 ++- frontend/src/utils/matchSocket.ts | 3 ++- frontend/src/utils/token.ts | 13 +++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 frontend/src/utils/token.ts diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 1d252552cc..5974fc1def 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -3,6 +3,7 @@ import { io } from "socket.io-client"; import { updateCursor, Cursor } from "./collabCursor"; import { Doc, Text, applyUpdateV2 } from "yjs"; import { Awareness } from "y-protocols/awareness"; +import { getToken } from "./token"; export enum CollabEvents { // Send @@ -38,7 +39,7 @@ export const collabSocket = io(COLLAB_SOCKET_URL, { reconnectionAttempts: 3, autoConnect: false, auth: { - token: `Bearer ${localStorage.getItem("token")}`, + token: getToken(), }, }); diff --git a/frontend/src/utils/communicationSocket.ts b/frontend/src/utils/communicationSocket.ts index cff0006ddb..93104b9658 100644 --- a/frontend/src/utils/communicationSocket.ts +++ b/frontend/src/utils/communicationSocket.ts @@ -1,4 +1,5 @@ import { io } from "socket.io-client"; +import { getToken } from "./token"; export enum CommunicationEvents { // send @@ -22,6 +23,6 @@ export const communicationSocket = io(COMMUNICATION_SOCKET_URL, { autoConnect: false, withCredentials: true, auth: { - token: `Bearer ${localStorage.getItem("token")}`, + token: getToken(), }, }); diff --git a/frontend/src/utils/matchSocket.ts b/frontend/src/utils/matchSocket.ts index 4ef4d2038b..d11749d05e 100644 --- a/frontend/src/utils/matchSocket.ts +++ b/frontend/src/utils/matchSocket.ts @@ -1,4 +1,5 @@ import { io } from "socket.io-client"; +import { getToken } from "./token"; const MATCH_SOCKET_URL = "http://localhost:3002"; @@ -6,6 +7,6 @@ export const matchSocket = io(MATCH_SOCKET_URL, { reconnectionAttempts: 3, autoConnect: false, auth: { - token: `Bearer ${localStorage.getItem("token")}`, + token: getToken(), }, }); diff --git a/frontend/src/utils/token.ts b/frontend/src/utils/token.ts new file mode 100644 index 0000000000..11998b856b --- /dev/null +++ b/frontend/src/utils/token.ts @@ -0,0 +1,13 @@ +export const setToken = (token: string) => { + const bearerToken = `Bearer ${token}`; + localStorage.setItem("accessToken", bearerToken); +}; + +export const getToken = () => { + const token = localStorage.getItem("accessToken"); + return token; +}; + +export const removeToken = () => { + localStorage.removeItem("accessToken"); +}; From a3cfa624f301e1649c71de9047c47b1e95732a74 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:11:14 +0800 Subject: [PATCH 135/192] clean up code --- .../src/handlers/websocketHandler.ts | 1 - .../controllers/questionHistoryController.ts | 18 ------------------ frontend/src/contexts/CollabContext.tsx | 2 +- frontend/src/reducers/qnHistoryReducer.ts | 15 --------------- 4 files changed, 1 insertion(+), 35 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 38a3c1e982..d4eaa43711 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -83,7 +83,6 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { language: language, }) .then((res) => { - console.log("created in collab"); doc.transact(() => { doc.getText().insert(0, template); }); diff --git a/backend/qn-history-service/src/controllers/questionHistoryController.ts b/backend/qn-history-service/src/controllers/questionHistoryController.ts index ad070f6615..2b3da8d927 100644 --- a/backend/qn-history-service/src/controllers/questionHistoryController.ts +++ b/backend/qn-history-service/src/controllers/questionHistoryController.ts @@ -27,7 +27,6 @@ export const createQnHistory = async ( timeTaken, code, language, - //compilerRes, } = req.body; const newQnHistory = new QnHistory({ @@ -39,7 +38,6 @@ export const createQnHistory = async ( timeTaken, code, language, - //compilerRes, }); await newQnHistory.save(); @@ -237,21 +235,5 @@ const formatQnHistoryResponse = (qnHistory: IQnHistory) => { timeTaken: qnHistory.timeTaken, code: qnHistory.code, language: qnHistory.language, - //compilerRes: qnHistory.compilerRes.map(formatCompilerRes), }; }; - -/*const formatCompilerRes = (compilerRes: ICompilerRes) => { - return { - status: compilerRes.status, - exception: compilerRes.exception, - stdout: compilerRes.stdout, - stderr: compilerRes.stderr, - executionTime: compilerRes.executionTime, - stdin: compilerRes.stdin, - stout: compilerRes.stdout, - actualResult: compilerRes.actualResult, - expectedResult: compilerRes.expectedResult, - isMatch: compilerRes.isMatch, - }; -};*/ diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 2ebb2fdaa2..0e1d010f18 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -11,7 +11,7 @@ import { import { toast } from "react-toastify"; import { useMatch } from "./MatchContext"; -import { qnHistoryClient, codeExecutionClient } from "../utils/api"; +import { codeExecutionClient } from "../utils/api"; import { useReducer } from "react"; import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; diff --git a/frontend/src/reducers/qnHistoryReducer.ts b/frontend/src/reducers/qnHistoryReducer.ts index 7a89ecd7e0..5a9564079c 100644 --- a/frontend/src/reducers/qnHistoryReducer.ts +++ b/frontend/src/reducers/qnHistoryReducer.ts @@ -2,19 +2,6 @@ import { Dispatch } from "react"; import { qnHistoryClient } from "../utils/api"; import { isString, isStringArray } from "../utils/typeChecker"; -/*type CompilerResult = { - status?: string; - exception?: string; - stdout?: string; - stderr?: string; - executionTime?: number; - stdin: string; - stout?: string; - actualResult?: string; - expectedResult: string; - isMatch?: boolean; -};*/ - type QnHistoryDetail = { id: string; userIds: Array; @@ -25,7 +12,6 @@ type QnHistoryDetail = { timeTaken: number; code: string; language: string; - //compilerRes: CompilerResult; }; type QnHistoryList = { @@ -119,7 +105,6 @@ export const createQnHistory = async ( timeTaken: qnHistory.timeTaken, code: qnHistory.code, language: qnHistory.language, - //compilerRes: qnHistory.compilerRes, }) .then((res) => { dispatch({ From cf24c32dc111b7d66a7cf9f1b98df4849fd61f7f Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Thu, 7 Nov 2024 22:55:30 +0800 Subject: [PATCH 136/192] Add verifytoken check for qn history -Add verifytoken check before creating and updating question history -Removing unnecessary API -fix bug with hasSubmitted --- .../src/api/questionHistoryService.ts | 31 +++++--- .../src/handlers/websocketHandler.ts | 5 +- .../src/api/questionHistoryService.ts | 31 -------- backend/qn-history-service/.env.sample | 3 + .../qn-history-service/src/api/userService.ts | 12 +++ .../controllers/questionHistoryController.ts | 25 ------ .../src/middlewares/basicAccessControl.ts | 17 ++++ .../src/routes/questionHistoryRoutes.ts | 8 +- .../qn-history-service/src/utils/constants.ts | 2 - backend/qn-history-service/swagger.yml | 34 -------- .../tests/qnHistoryRoutes.spec.ts | 36 --------- frontend/src/contexts/CollabContext.tsx | 6 +- frontend/src/reducers/qnHistoryReducer.ts | 78 ++++--------------- 13 files changed, 77 insertions(+), 211 deletions(-) delete mode 100644 backend/matching-service/src/api/questionHistoryService.ts create mode 100644 backend/qn-history-service/src/api/userService.ts create mode 100644 backend/qn-history-service/src/middlewares/basicAccessControl.ts diff --git a/backend/collab-service/src/api/questionHistoryService.ts b/backend/collab-service/src/api/questionHistoryService.ts index e8da853d7d..2043575cf5 100644 --- a/backend/collab-service/src/api/questionHistoryService.ts +++ b/backend/collab-service/src/api/questionHistoryService.ts @@ -17,17 +17,26 @@ export const createQuestionHistory = ( title: string, submissionStatus: string, code: string, - language: string + language: string, + authToken: string ) => { const dateAttempted = new Date(); - return qnHistoryService.post("/", { - userIds, - questionId, - title, - submissionStatus, - dateAttempted, - timeTaken: 0, - code, - language, - }); + return qnHistoryService.post( + "/", + { + userIds, + questionId, + title, + submissionStatus, + dateAttempted, + timeTaken: 0, + code, + language, + }, + { + headers: { + Authorization: authToken, + }, + } + ); }; diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 70161d0167..88db617c04 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -71,13 +71,16 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { const isPartnerReady = partnerReadiness.get(roomId); if (isPartnerReady && doc.getText().length === 0) { + const token = + socket.handshake.headers.authorization || socket.handshake.auth.token; createQuestionHistory( [uid1, uid2], qnId, qnTitle, "Attempted", template, - language + language, + token ) .then((res) => { doc.transact(() => { diff --git a/backend/matching-service/src/api/questionHistoryService.ts b/backend/matching-service/src/api/questionHistoryService.ts deleted file mode 100644 index 0a17c16703..0000000000 --- a/backend/matching-service/src/api/questionHistoryService.ts +++ /dev/null @@ -1,31 +0,0 @@ -import axios from "axios"; - -const QN_HISTORY_SERVICE_URL = - process.env.QN_HISTORY_SERVICE_URL || - "http://qn-history-service:3006/api/qnhistories"; - -const qnHistoryService = axios.create({ - baseURL: QN_HISTORY_SERVICE_URL, - headers: { - "Content-Type": "application/json", - }, -}); - -export const createQuestionHistory = ( - questionId: string, - title: string, - submissionStatus: string, - language: string, - ...userIds: string[] -) => { - const dateAttempted = new Date(); - return qnHistoryService.post("/", { - userIds, - questionId, - title, - submissionStatus, - language, - dateAttempted, - timeTaken: 0, - }); -}; diff --git a/backend/qn-history-service/.env.sample b/backend/qn-history-service/.env.sample index 097fb51765..92424c8b48 100644 --- a/backend/qn-history-service/.env.sample +++ b/backend/qn-history-service/.env.sample @@ -4,6 +4,9 @@ SERVICE_PORT=3006 # Origins for cors ORIGINS=http://localhost:5173,http://127.0.0.1:5173 +# Other services +USER_SERVICE_URL=http://user-service:3001/api + # Tests MONGO_URI_TEST=mongodb://mongo:mongo@test-mongo:27017/ diff --git a/backend/qn-history-service/src/api/userService.ts b/backend/qn-history-service/src/api/userService.ts new file mode 100644 index 0000000000..7e1459f114 --- /dev/null +++ b/backend/qn-history-service/src/api/userService.ts @@ -0,0 +1,12 @@ +import axios from "axios"; +import dotenv from "dotenv"; + +dotenv.config(); + +const USER_SERVICE_URL = + process.env.USER_SERVICE_URL || "http://user-service:3001/api"; + +export const userClient = axios.create({ + baseURL: USER_SERVICE_URL, + withCredentials: true, +}); diff --git a/backend/qn-history-service/src/controllers/questionHistoryController.ts b/backend/qn-history-service/src/controllers/questionHistoryController.ts index 2b3da8d927..d4d76dc036 100644 --- a/backend/qn-history-service/src/controllers/questionHistoryController.ts +++ b/backend/qn-history-service/src/controllers/questionHistoryController.ts @@ -83,31 +83,6 @@ export const updateQnHistory = async ( } }; -export const deleteQnHistory = async ( - req: Request, - res: Response -): Promise => { - try { - const { id } = req.params; - - if (!id.match(MONGO_OBJ_ID_FORMAT)) { - res.status(400).json({ message: MONGO_OBJ_ID_MALFORMED_MESSAGE }); - return; - } - - const currQnHistory = await QnHistory.findById(id); - if (!currQnHistory) { - res.status(404).json({ message: QN_HIST_NOT_FOUND_MESSAGE }); - return; - } - - await QnHistory.findByIdAndDelete(id); - res.status(200).json({ message: QN_HIST_DELETED_MESSAGE }); - } catch (error) { - res.status(500).json({ message: SERVER_ERROR_MESSAGE, error }); - } -}; - type QnHistListParams = { page: string; qnHistLimit: string; diff --git a/backend/qn-history-service/src/middlewares/basicAccessControl.ts b/backend/qn-history-service/src/middlewares/basicAccessControl.ts new file mode 100644 index 0000000000..1053b25a53 --- /dev/null +++ b/backend/qn-history-service/src/middlewares/basicAccessControl.ts @@ -0,0 +1,17 @@ +import { NextFunction, Response, Request } from "express"; +import { userClient } from "../api/userService"; + +export const verifyToken = ( + req: Request, + res: Response, + next: NextFunction +) => { + const authHeader = req.headers.authorization; + userClient + .get("/auth/verify-token", { headers: { Authorization: authHeader } }) + .then(() => next()) + .catch((err) => { + console.log(err.response); + return res.status(err.response.status).json(err.response.data); + }); +}; diff --git a/backend/qn-history-service/src/routes/questionHistoryRoutes.ts b/backend/qn-history-service/src/routes/questionHistoryRoutes.ts index 4395263206..89afc253f9 100644 --- a/backend/qn-history-service/src/routes/questionHistoryRoutes.ts +++ b/backend/qn-history-service/src/routes/questionHistoryRoutes.ts @@ -1,22 +1,20 @@ import express from "express"; import { createQnHistory, - deleteQnHistory, readQnHistIndiv, readQnHistoryList, updateQnHistory, } from "../controllers/questionHistoryController"; +import { verifyToken } from "../middlewares/basicAccessControl"; const router = express.Router(); -router.post("/", createQnHistory); +router.post("/", verifyToken, createQnHistory); -router.put("/:id", updateQnHistory); +router.put("/:id", verifyToken, updateQnHistory); router.get("/", readQnHistoryList); router.get("/:id", readQnHistIndiv); -router.delete("/:id", deleteQnHistory); - export default router; diff --git a/backend/qn-history-service/src/utils/constants.ts b/backend/qn-history-service/src/utils/constants.ts index c05ca2ba83..1c77c90c0e 100644 --- a/backend/qn-history-service/src/utils/constants.ts +++ b/backend/qn-history-service/src/utils/constants.ts @@ -2,8 +2,6 @@ export const QN_HIST_CREATED_MESSAGE = "Question history created successfully."; export const QN_HIST_NOT_FOUND_MESSAGE = "Question history not found."; -export const QN_HIST_DELETED_MESSAGE = "Question history deleted successfully."; - export const SERVER_ERROR_MESSAGE = "Server error."; export const QN_HIST_RETRIEVED_MESSAGE = diff --git a/backend/qn-history-service/swagger.yml b/backend/qn-history-service/swagger.yml index 10a229b72c..ab684a877f 100644 --- a/backend/qn-history-service/swagger.yml +++ b/backend/qn-history-service/swagger.yml @@ -255,40 +255,6 @@ paths: application/json: schema: $ref: "#/definitions/ServerError" - delete: - tags: - - qnhistories - summary: Deletes a question history - description: Deletes a question history - parameters: - - in: path - name: id - type: string - required: true - description: Question history id - responses: - 200: - description: Successful Response - content: - application/json: - schema: - type: object - properties: - message: - type: string - description: Message - 404: - description: Question History Not Found - content: - application/json: - schema: - $ref: "#/definitions/Error" - 500: - description: Internal Server Error - content: - application/json: - schema: - $ref: "#/definitions/ServerError" get: tags: - qnhistories diff --git a/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts b/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts index 82245f8281..303dc789a5 100644 --- a/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts +++ b/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts @@ -243,40 +243,4 @@ describe("Qn History Routes", () => { expect(res.body.message).toBe(QN_HIST_NOT_FOUND_MESSAGE); }); }); - - describe("DELETE /:id", () => { - it("Deletes existing qn history", async () => { - const userIds = ["66f77e9f27ab3f794bdae664", "66f77e9f27ab3f794bdae665"]; - const questionId = "66f77e9f27ab3f794bdae666"; - const title = faker.lorem.lines(1); - const submissionStatus = "Attempted"; - const dateAttempted = new Date(); - const timeTaken = 0; - const language = "Python"; - const newQnHistory = new QnHistory({ - userIds, - questionId, - title, - submissionStatus, - dateAttempted, - timeTaken, - language, - }); - await newQnHistory.save(); - const res = await request.delete(`${BASE_URL}/${newQnHistory.id}`); - expect(res.status).toBe(200); - }); - - it("Does not delete non-existing qn history with invalid object id", async () => { - const res = await request.delete(`${BASE_URL}/blah`); - expect(res.status).toBe(400); - expect(res.body.message).toBe(MONGO_OBJ_ID_MALFORMED_MESSAGE); - }); - - it("Does not delete non-existing qn history with valid object id", async () => { - const res = await request.delete(`${BASE_URL}/66f77e9f27ab3f794bdae664`); - expect(res.status).toBe(404); - expect(res.body.message).toBe(QN_HIST_NOT_FOUND_MESSAGE); - }); - }); }); diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index c87b3e0e7e..7d5ab7446f 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -92,7 +92,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [isEndSessionModalOpen, setIsEndSessionModalOpen] = useState(false); const [qnHistoryId, setQnHistoryId] = useState(null); - let hasSubmitted: boolean = false; + const [hasSubmitted, setHasSubmitted] = useState(false); const handleSubmitSessionClick = async () => { try { @@ -102,10 +102,10 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { code: code.replace(/\t/g, " ".repeat(4)), language: matchCriteria?.language.toLowerCase(), }); - hasSubmitted = true; + setHasSubmitted(true); console.log([...res.data.data]); setCompilerResult([...res.data.data]); - + let isMatch = true; for (let i = 0; i < res.data.data.length; i++) { if (!res.data.data[i].isMatch) { diff --git a/frontend/src/reducers/qnHistoryReducer.ts b/frontend/src/reducers/qnHistoryReducer.ts index 5a9564079c..78fd0d4635 100644 --- a/frontend/src/reducers/qnHistoryReducer.ts +++ b/frontend/src/reducers/qnHistoryReducer.ts @@ -20,11 +20,9 @@ type QnHistoryList = { }; enum QnHistoryActionTypes { - CREATE_QNHIST = "create_qnhist", VIEW_QNHIST_LIST = "view_qnhist_list", VIEW_QNHIST = "view_qnhist", UPDATE_QNHIST = "update_qnhist", - ERROR_CREATING_QNHIST = "error_creating_qnhist", ERROR_FETCHING_QNHIST_LIST = "error_fetching_qnhist_list", ERROR_FETCHING_SELECTED_QNHIST = "error_fetching_selected_qnhist", ERROR_UPDATING_QNHIST = "error_updating_qnhist", @@ -91,37 +89,6 @@ export const initialQHState: QnHistoriesState = { selectedQnHistoryError: null, }; -export const createQnHistory = async ( - qnHistory: Omit, - dispatch: Dispatch -): Promise => { - return qnHistoryClient - .post("/", { - userIds: qnHistory.userIds, - questionId: qnHistory.questionId, - title: qnHistory.title, - submissionStatus: qnHistory.submissionStatus, - dateAttempted: qnHistory.dateAttempted, - timeTaken: qnHistory.timeTaken, - code: qnHistory.code, - language: qnHistory.language, - }) - .then((res) => { - dispatch({ - type: QnHistoryActionTypes.CREATE_QNHIST, - payload: res.data, - }); - return res.data.qnHistory.id; - }) - .catch((err) => { - dispatch({ - type: QnHistoryActionTypes.ERROR_CREATING_QNHIST, - payload: err.response?.data.message || err.message, - }); - return ""; - }); -}; - export const getQnHistoryList = ( page: number, qnHistLimit: number, @@ -143,7 +110,6 @@ export const getQnHistoryList = ( }, }) .then((res) => { - console.log(res.data); dispatch({ type: QnHistoryActionTypes.VIEW_QNHIST_LIST, payload: res.data, @@ -185,13 +151,22 @@ export const updateQnHistoryById = async ( >, dispatch: Dispatch ): Promise => { + const accessToken = localStorage.getItem("token"); return qnHistoryClient - .put(`/${qnHistoryId}`, { - submissionStatus: qnHistory.submissionStatus, - dateAttempted: qnHistory.dateAttempted, - timeTaken: qnHistory.timeTaken, - code: qnHistory.code, - }) + .put( + `/${qnHistoryId}`, + { + submissionStatus: qnHistory.submissionStatus, + dateAttempted: qnHistory.dateAttempted, + timeTaken: qnHistory.timeTaken, + code: qnHistory.code, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ) .then((res) => { dispatch({ type: QnHistoryActionTypes.UPDATE_QNHIST, @@ -208,15 +183,6 @@ export const updateQnHistoryById = async ( }); }; -export const deleteQuestionById = async (qnHistoryId: string) => { - try { - await qnHistoryClient.delete(`/${qnHistoryId}`); - return true; - } catch { - return false; - } -}; - export const setSelectedQnHistoryError = ( error: string, dispatch: React.Dispatch @@ -234,13 +200,6 @@ const qnHistoryReducer = ( const { type } = action; switch (type) { - case QnHistoryActionTypes.CREATE_QNHIST: { - const { payload } = action; - if (!isQnHistory(payload)) { - return state; - } - return { ...state, qnHistories: [payload, ...state.qnHistories] }; - } case QnHistoryActionTypes.VIEW_QNHIST_LIST: { const { payload } = action; if (!isQnHistoryList(payload)) { @@ -271,13 +230,6 @@ const qnHistoryReducer = ( ), }; } - case QnHistoryActionTypes.ERROR_CREATING_QNHIST: { - const { payload } = action; - if (!isString(payload)) { - return state; - } - return { ...state, selectedQnHistoryError: payload }; - } case QnHistoryActionTypes.ERROR_FETCHING_QNHIST_LIST: { const { payload } = action; if (!isString(payload)) { From df8ef53e3a77085d60a2e3c6efaeda2271895456 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Thu, 7 Nov 2024 23:00:11 +0800 Subject: [PATCH 137/192] Fix lint error --- backend/matching-service/src/server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/matching-service/src/server.ts b/backend/matching-service/src/server.ts index 0c420ce87a..beb189dc0b 100644 --- a/backend/matching-service/src/server.ts +++ b/backend/matching-service/src/server.ts @@ -3,7 +3,6 @@ import app, { allowedOrigins } from "./app.ts"; import { handleWebsocketMatchEvents } from "./handlers/websocketHandler.ts"; import { Server } from "socket.io"; import { connectToRabbitMq } from "./config/rabbitmq.ts"; -import { verifyToken } from "./api/userService.ts"; import { verifyUserToken } from "./middlewares/basicAccessControl.ts"; const server = http.createServer(app); From 4f18fb76ae8373ddb7d86c86fed3ef076c6faa1b Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Fri, 8 Nov 2024 00:16:53 +0800 Subject: [PATCH 138/192] Remove unused import --- backend/matching-service/src/server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/matching-service/src/server.ts b/backend/matching-service/src/server.ts index 0c420ce87a..beb189dc0b 100644 --- a/backend/matching-service/src/server.ts +++ b/backend/matching-service/src/server.ts @@ -3,7 +3,6 @@ import app, { allowedOrigins } from "./app.ts"; import { handleWebsocketMatchEvents } from "./handlers/websocketHandler.ts"; import { Server } from "socket.io"; import { connectToRabbitMq } from "./config/rabbitmq.ts"; -import { verifyToken } from "./api/userService.ts"; import { verifyUserToken } from "./middlewares/basicAccessControl.ts"; const server = http.createServer(app); From 4e1c332399addf514fad1395fe4c2d3439d65d29 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Fri, 8 Nov 2024 01:56:33 +0800 Subject: [PATCH 139/192] Add pop up before session end --- .../src/handlers/websocketHandler.ts | 53 ++++---- .../src/components/CustomDialog/index.tsx | 45 +++++-- frontend/src/contexts/CollabContext.tsx | 116 ++++++++++++------ frontend/src/pages/CollabSandbox/index.tsx | 25 +++- frontend/src/utils/collabSocket.ts | 15 ++- frontend/src/utils/constants.ts | 4 +- 6 files changed, 169 insertions(+), 89 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 184b6a890a..f30009ffb6 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -12,12 +12,13 @@ enum CollabEvents { UPDATE_REQUEST = "update_request", UPDATE_CURSOR_REQUEST = "update_cursor_request", RECONNECT_REQUEST = "reconnect_request", + END_SESSION_REQUEST = "end_session_request", // Send ROOM_READY = "room_ready", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", - PARTNER_LEFT = "partner_left", + END_SESSION = "end_session", PARTNER_DISCONNECTED = "partner_disconnected", } @@ -92,23 +93,38 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { socket.on( CollabEvents.LEAVE, - (uid: string, roomId: string, isIntentional: boolean) => { + (uid: string, roomId: string, isPartnerNotified: boolean) => { + console.log("leave: ", uid); const connectionKey = `${uid}:${roomId}`; - if (isIntentional || !userConnections.has(connectionKey)) { - handleUserLeave(uid, roomId, socket, isIntentional); - return; + if (userConnections.has(connectionKey)) { + console.log("clear disconnect timeout: ", uid); + clearTimeout(userConnections.get(connectionKey)!); } - clearTimeout(userConnections.get(connectionKey)!); + if (isPartnerNotified || !userConnections.has(connectionKey)) { + handleUserLeave(uid, roomId, socket); + return; + } + console.log("set disconnect timeout: ", uid); const connectionTimeout = setTimeout(() => { - handleUserLeave(uid, roomId, socket, isIntentional); + handleUserLeave(uid, roomId, socket); + console.log("notify partner of disconnect: ", uid); + console.log("room: ", roomId); + io.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED); }, CONNECTION_DELAY); userConnections.set(connectionKey, connectionTimeout); } ); + socket.on( + CollabEvents.END_SESSION_REQUEST, + (roomId: string, timeTaken: number) => { + socket.to(roomId).emit(CollabEvents.END_SESSION, timeTaken); + } + ); + socket.on(CollabEvents.RECONNECT_REQUEST, async (roomId: string) => { // TODO: Handle recconnection socket.join(roomId); @@ -165,29 +181,12 @@ const saveDocument = async (roomId: string, doc: Doc) => { }); }; -const handleUserLeave = ( - uid: string, - roomId: string, - socket: Socket, - isIntentional: boolean -) => { +const handleUserLeave = (uid: string, roomId: string, socket: Socket) => { const connectionKey = `${uid}:${roomId}`; - if (userConnections.has(connectionKey)) { - clearTimeout(userConnections.get(connectionKey)!); - userConnections.delete(connectionKey); - } + userConnections.delete(connectionKey); socket.leave(roomId); socket.disconnect(); - const room = io.sockets.adapter.rooms.get(roomId); - if (!room || room.size === 0) { - removeCollabSession(roomId); - } else { - io.to(roomId).emit( - isIntentional - ? CollabEvents.PARTNER_LEFT - : CollabEvents.PARTNER_DISCONNECTED - ); - } + removeCollabSession(roomId); }; diff --git a/frontend/src/components/CustomDialog/index.tsx b/frontend/src/components/CustomDialog/index.tsx index 611f296e62..9ed355991b 100644 --- a/frontend/src/components/CustomDialog/index.tsx +++ b/frontend/src/components/CustomDialog/index.tsx @@ -12,9 +12,9 @@ type CustomDialogProps = { bodyText: React.ReactNode; primaryAction: string; handlePrimaryAction: () => void; - secondaryAction: string; + secondaryAction?: string; open: boolean; - handleClose: () => void; + handleClose?: () => void; }; const CustomDialog: React.FC = (props) => { @@ -38,11 +38,27 @@ const CustomDialog: React.FC = (props) => { open={open} onClose={handleClose} > - + {titleText} - + {bodyText} @@ -52,18 +68,23 @@ const CustomDialog: React.FC = (props) => { paddingBottom: theme.spacing(2.5), })} > - + {secondaryAction ? ( + + ) : ( + <> + )} diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 00a4a3b657..5bb0e94e14 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -1,6 +1,12 @@ /* eslint-disable react-refresh/only-export-components */ -import React, { createContext, useContext, useEffect, useState } from "react"; +import React, { + createContext, + useContext, + useEffect, + useRef, + useState, +} from "react"; import { USE_MATCH_ERROR_MESSAGE, FAILED_TESTCASE_MESSAGE, @@ -41,7 +47,10 @@ type CollabContextType = { handleSubmitSessionClick: () => void; handleEndSessionClick: () => void; handleRejectEndSession: () => void; - handleConfirmEndSession: () => void; + handleConfirmEndSession: ( + isInitiatedByPartner: boolean, + time?: number + ) => void; checkPartnerStatus: () => void; setCode: React.Dispatch>; compilerResult: CompilerResult[]; @@ -49,6 +58,8 @@ type CollabContextType = { isEndSessionModalOpen: boolean; time: number; resetCollab: () => void; + handleExitSession: () => void; + isExitSessionModalOpen: boolean; }; const CollabContext = createContext(null); @@ -72,17 +83,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { qnHistoryId, } = match; - const [time, setTime] = useState(0); - - useEffect(() => { - const intervalId = setInterval( - () => setTime((prevTime) => prevTime + 1), - 1000 - ); - - return () => clearInterval(intervalId); - }, [time]); - // eslint-disable-next-line const [_qnHistoryState, qnHistoryDispatch] = useReducer( qnHistoryReducer, @@ -92,6 +92,31 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [compilerResult, setCompilerResult] = useState([]); const [isEndSessionModalOpen, setIsEndSessionModalOpen] = useState(false); + const [isExitSessionModalOpen, setIsExitSessionModalOpen] = + useState(false); + const [time, setTime] = useState(0); + const [stopTime, setStopTime] = useState(true); + + const timeRef = useRef(time); + const codeRef = useRef(code); + + useEffect(() => { + timeRef.current = time; + codeRef.current = code; + }, [time, code]); + + useEffect(() => { + if (stopTime) { + return; + } + + const intervalId = setInterval( + () => setTime((prevTime) => prevTime + 1), + 1000 + ); + + return () => clearInterval(intervalId); + }, [time, stopTime]); const handleSubmitSessionClick = async () => { try { @@ -141,7 +166,10 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setIsEndSessionModalOpen(false); }; - const handleConfirmEndSession = async () => { + const handleConfirmEndSession = async ( + isInitiatedByPartner: boolean, + timeTaken?: number + ) => { setIsEndSessionModalOpen(false); const roomId = getMatchId(); @@ -150,29 +178,44 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { return; } - // Get question history - const data = await qnHistoryClient.get(qnHistoryId); + setStopTime(true); + setIsExitSessionModalOpen(true); + + if (isInitiatedByPartner && timeTaken) { + setTime(timeTaken); + } else { + // Get question history + const data = await qnHistoryClient.get(qnHistoryId); + + // Only update question history if it has not been submitted before + if (!data.data.qnHistory.code) { + updateQnHistoryById( + qnHistoryId as string, + { + submissionStatus: "Attempted", + dateAttempted: new Date().toISOString(), + timeTaken: timeRef.current, + code: codeRef.current.replace(/\t/g, " ".repeat(4)), + }, + qnHistoryDispatch + ); + } + } - // Only update question history if it has not been submitted before - if (!data.data.qnHistory.code) { - updateQnHistoryById( - qnHistoryId as string, - { - submissionStatus: "Attempted", - dateAttempted: new Date().toISOString(), - timeTaken: time, - code: code.replace(/\t/g, " ".repeat(4)), - }, - qnHistoryDispatch - ); + if (!isInitiatedByPartner) { + // Notify partner + collabSocket.emit(CollabEvents.END_SESSION_REQUEST, roomId, time); } // Leave collaboration room - leave(matchUser.id as string, roomId, true); - // TODO: partner leave + leave(matchUser.id, roomId, true); // Leave chat room communicationSocket.emit(CommunicationEvents.USER_DISCONNECT); + }; + + const handleExitSession = () => { + setIsExitSessionModalOpen(false); // Delete match data stopMatch(); @@ -183,24 +226,21 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const checkPartnerStatus = () => { - collabSocket.on(CollabEvents.PARTNER_LEFT, () => { + collabSocket.on(CollabEvents.END_SESSION, (timeTaken: number) => { toast.info(COLLAB_ENDED_MESSAGE); - setIsEndSessionModalOpen(false); - stopMatch(); - appNavigate("/home"); + handleConfirmEndSession(true, timeTaken); }); collabSocket.on(CollabEvents.PARTNER_DISCONNECTED, () => { toast.error(COLLAB_PARTNER_DISCONNECTED_MESSAGE); - setIsEndSessionModalOpen(false); - stopMatch(); - appNavigate("/home"); + handleConfirmEndSession(true); }); }; const resetCollab = () => { setCompilerResult([]); setTime(0); + setStopTime(false); }; return ( @@ -217,6 +257,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { isEndSessionModalOpen, time, resetCollab, + handleExitSession, + isExitSessionModalOpen, }} > {children} diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 852043fe26..c4c395da39 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -23,6 +23,10 @@ import CodeEditor from "../../components/CodeEditor"; import { CollabSessionData, join, leave } from "../../utils/collabSocket"; import { toast } from "react-toastify"; import CustomDialog from "../../components/CustomDialog"; +import { + extractMinutesFromTime, + extractSecondsFromTime, +} from "../../utils/sessionTime"; const CollabSandbox: React.FC = () => { const match = useMatch(); @@ -44,6 +48,9 @@ const CollabSandbox: React.FC = () => { checkPartnerStatus, isEndSessionModalOpen, resetCollab, + handleExitSession, + isExitSessionModalOpen, + time, } = collab; const [state, dispatch] = useReducer(reducer, initialState); @@ -57,13 +64,13 @@ const CollabSandbox: React.FC = () => { const matchId = getMatchId(); useEffect(() => { + resetCollab(); + if (!questionId) { return; } getQuestionById(questionId, dispatch); - resetCollab(); - if (!matchUser || !matchId) { return; } @@ -88,12 +95,12 @@ const CollabSandbox: React.FC = () => { // handle page refresh / tab closure const handleUnload = () => { - leave(matchUser.id, matchId); + leave(matchUser.id, matchId, false); }; window.addEventListener("unload", handleUnload); return () => { - leave(matchUser.id, matchId); + leave(matchUser.id, matchId, false); window.removeEventListener("unload", handleUnload); }; @@ -120,11 +127,19 @@ const CollabSandbox: React.FC = () => { } primaryAction="Confirm" - handlePrimaryAction={handleConfirmEndSession} + handlePrimaryAction={() => handleConfirmEndSession(false)} secondaryAction="Cancel" open={isEndSessionModalOpen} handleClose={handleRejectEndSession} /> + { }); }; -export const leave = (uid: string, roomId: string, isIntentional?: boolean) => { +export const leave = ( + uid: string, + roomId: string, + isPartnerNotified: boolean +) => { collabSocket.removeAllListeners(); collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_SUCCESS); collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_FAILED); - collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isIntentional); - if (doc) { - doc.destroy(); - } + collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isPartnerNotified); + doc?.destroy(); }; export const sendCursorUpdate = (roomId: string, cursor: Cursor) => { diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 715c3ef3be..87370bb819 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -104,9 +104,9 @@ export const QUESTION_DOES_NOT_EXIST_ERROR = // Collab export const COLLAB_ENDED_MESSAGE = - "Your partner has ended the collaboration session. The session will be closing soon..."; + "Your partner has ended the collaboration session."; export const COLLAB_PARTNER_DISCONNECTED_MESSAGE = - "Your partner has disconnected! The session will be closing soon..."; + "Unfortunately, the collaboration session has ended as your partner has disconnected."; export const COLLAB_CONNECTION_ERROR = "Error connecting you to the collaboration session! Please try again."; export const COLLAB_END_ERROR = From 40fd3f0d696e96e324710ea31483e2973f351d18 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Fri, 8 Nov 2024 03:13:44 +0800 Subject: [PATCH 140/192] Define types in separate file --- .../src/controllers/questionHistoryController.ts | 11 +---------- backend/qn-history-service/src/utils/types.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 backend/qn-history-service/src/utils/types.ts diff --git a/backend/qn-history-service/src/controllers/questionHistoryController.ts b/backend/qn-history-service/src/controllers/questionHistoryController.ts index d4d76dc036..f33344179c 100644 --- a/backend/qn-history-service/src/controllers/questionHistoryController.ts +++ b/backend/qn-history-service/src/controllers/questionHistoryController.ts @@ -7,11 +7,11 @@ import { PAGE_LIMIT_INCORRECT_FORMAT_MESSAGE, PAGE_LIMIT_USERID_ORDER_REQUIRED_MESSAGE, QN_HIST_CREATED_MESSAGE, - QN_HIST_DELETED_MESSAGE, QN_HIST_NOT_FOUND_MESSAGE, QN_HIST_RETRIEVED_MESSAGE, SERVER_ERROR_MESSAGE, } from "../utils/constants.ts"; +import { QnHistListParams } from "../utils/types.ts"; export const createQnHistory = async ( req: Request, @@ -83,15 +83,6 @@ export const updateQnHistory = async ( } }; -type QnHistListParams = { - page: string; - qnHistLimit: string; - userId: string; - title: string; //qn title search keyword - status: string; //submission status - order: string; //entries sort order -}; - export const readQnHistoryList = async ( req: Request, res: Response diff --git a/backend/qn-history-service/src/utils/types.ts b/backend/qn-history-service/src/utils/types.ts new file mode 100644 index 0000000000..31b5e6a4bc --- /dev/null +++ b/backend/qn-history-service/src/utils/types.ts @@ -0,0 +1,8 @@ +export type QnHistListParams = { + page: string; + qnHistLimit: string; + userId: string; + title: string; //qn title search keyword + status: string; //submission status + order: string; //entries sort order +}; From 14293512db56a5c58312ca708bc0ab1203f0d217 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Fri, 8 Nov 2024 04:01:43 +0800 Subject: [PATCH 141/192] Fix collab page stuck on loading --- .../collab-service/src/handlers/websocketHandler.ts | 7 +------ frontend/src/contexts/CollabContext.tsx | 6 ++++-- frontend/src/pages/CollabSandbox/index.tsx | 12 +++++++----- frontend/src/utils/collabSocket.ts | 5 ++++- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index f30009ffb6..402e36131b 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -94,23 +94,18 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { socket.on( CollabEvents.LEAVE, (uid: string, roomId: string, isPartnerNotified: boolean) => { - console.log("leave: ", uid); const connectionKey = `${uid}:${roomId}`; if (userConnections.has(connectionKey)) { - console.log("clear disconnect timeout: ", uid); clearTimeout(userConnections.get(connectionKey)!); } - if (isPartnerNotified || !userConnections.has(connectionKey)) { + if (isPartnerNotified) { handleUserLeave(uid, roomId, socket); return; } - console.log("set disconnect timeout: ", uid); const connectionTimeout = setTimeout(() => { handleUserLeave(uid, roomId, socket); - console.log("notify partner of disconnect: ", uid); - console.log("room: ", roomId); io.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED); }, CONNECTION_DELAY); diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 5bb0e94e14..d59691fbf9 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -226,12 +226,14 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const checkPartnerStatus = () => { - collabSocket.on(CollabEvents.END_SESSION, (timeTaken: number) => { + collabSocket.once(CollabEvents.END_SESSION, (timeTaken: number) => { + collabSocket.off(CollabEvents.PARTNER_DISCONNECTED); toast.info(COLLAB_ENDED_MESSAGE); handleConfirmEndSession(true, timeTaken); }); - collabSocket.on(CollabEvents.PARTNER_DISCONNECTED, () => { + collabSocket.once(CollabEvents.PARTNER_DISCONNECTED, () => { + collabSocket.off(CollabEvents.END_SESSION); toast.error(COLLAB_PARTNER_DISCONNECTED_MESSAGE); handleConfirmEndSession(true); }); diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index c4c395da39..a334654428 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -66,11 +66,6 @@ const CollabSandbox: React.FC = () => { useEffect(() => { resetCollab(); - if (!questionId) { - return; - } - getQuestionById(questionId, dispatch); - if (!matchUser || !matchId) { return; } @@ -107,6 +102,13 @@ const CollabSandbox: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + if (!questionId) { + return; + } + getQuestionById(questionId, dispatch); + }, [questionId]); + if (!matchUser || !matchCriteria || !matchId || !isConnecting) { return ; } diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 8f0b7595e9..7a7f0b9403 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -93,8 +93,11 @@ export const leave = ( collabSocket.removeAllListeners(); collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_SUCCESS); collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_FAILED); - collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isPartnerNotified); doc?.destroy(); + + if (collabSocket.connected) { + collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isPartnerNotified); + } }; export const sendCursorUpdate = (roomId: string, cursor: Cursor) => { From 0551489ca8163901d1eeb1fefedac0799ed3dac7 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Fri, 8 Nov 2024 14:48:23 +0800 Subject: [PATCH 142/192] Refactor env --- backend/user-service/src/model/repository.ts | 6 +++++- frontend/src/utils/api.ts | 13 +++++++++---- frontend/src/utils/collabSocket.ts | 3 ++- frontend/src/utils/communicationSocket.ts | 3 ++- frontend/src/utils/matchSocket.ts | 5 ++++- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/backend/user-service/src/model/repository.ts b/backend/user-service/src/model/repository.ts index 30b7b625c8..10794ba664 100644 --- a/backend/user-service/src/model/repository.ts +++ b/backend/user-service/src/model/repository.ts @@ -1,13 +1,17 @@ import UserModel, { IUser } from "./user-model"; -import "dotenv/config"; +import dotenv from "dotenv"; import { connect } from "mongoose"; +dotenv.config(); + export async function connectToDB() { const mongoDBUri: string | undefined = process.env.NODE_ENV === "production" ? process.env.MONGO_CLOUD_URI : process.env.MONGO_LOCAL_URI; + console.log(mongoDBUri); + if (!mongoDBUri) { throw new Error("MongoDB URI is not provided"); } diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index 8f834d5a9e..410f668de7 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -1,9 +1,14 @@ import axios from "axios"; -const usersUrl = "http://localhost:3001/api"; -const questionsUrl = "http://localhost:3000/api/questions"; -const codeExecutionUrl = "http://localhost:3004/api/run"; -const qnHistoriesUrl = "http://localhost:3006/api/qnhistories"; +const usersUrl = + import.meta.env.VITE_USER_SERVICE_URL ?? "http://localhost:3001/api"; +const questionsUrl = + import.meta.env.VITE_QN_SERVICE_URL ?? "http://localhost:3000/api/questions"; +const codeExecutionUrl = + import.meta.env.VITE_CODE_EXEC_SERVICE_URL ?? "http://localhost:3004/api/run"; +const qnHistoriesUrl = + import.meta.env.VITE_QN_HIST_SERVICE_URL ?? + "http://localhost:3006/api/qnhistories"; export const questionClient = axios.create({ baseURL: questionsUrl, diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 4fe50bafe3..2fc7907b50 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -32,7 +32,8 @@ export type CollabSessionData = { awareness: Awareness; }; -const COLLAB_SOCKET_URL = "http://localhost:3003"; +const COLLAB_SOCKET_URL = + import.meta.env.VITE_COLLAB_SERVICE_URL ?? "http://localhost:3003"; export const collabSocket = io(COLLAB_SOCKET_URL, { reconnectionAttempts: 3, autoConnect: false, diff --git a/frontend/src/utils/communicationSocket.ts b/frontend/src/utils/communicationSocket.ts index 46d179558d..b772680888 100644 --- a/frontend/src/utils/communicationSocket.ts +++ b/frontend/src/utils/communicationSocket.ts @@ -13,7 +13,8 @@ export enum CommunicationEvents { DISCONNECTED = "disconnected", } -const COMMUNICATION_SOCKET_URL = "http://localhost:3005"; +const COMMUNICATION_SOCKET_URL = + import.meta.env.VITE_COMM_SERVICE_URL ?? "http://localhost:3005"; export const communicationSocket = io(COMMUNICATION_SOCKET_URL, { reconnectionAttempts: 3, diff --git a/frontend/src/utils/matchSocket.ts b/frontend/src/utils/matchSocket.ts index e79801a6b4..879394832c 100644 --- a/frontend/src/utils/matchSocket.ts +++ b/frontend/src/utils/matchSocket.ts @@ -1,6 +1,9 @@ import { io } from "socket.io-client"; -const MATCH_SOCKET_URL = "http://localhost:3002"; +const MATCH_SOCKET_URL = + import.meta.env.VITE_MATCH_SERVICE_URL ?? "http://localhost:3002"; + +console.log(import.meta.env.VITE_MATCH_SERVICE_URL); export const matchSocket = io(MATCH_SOCKET_URL, { reconnectionAttempts: 3, From f490f9a1b1f0c90e6660941cb1aacd80edaaad53 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Fri, 8 Nov 2024 15:15:07 +0800 Subject: [PATCH 143/192] Refactor code --- frontend/src/contexts/AuthContext.tsx | 9 +++++---- frontend/src/contexts/ProfileContext.tsx | 9 +++++---- frontend/src/reducers/questionReducer.ts | 21 +++++++++++---------- frontend/src/utils/token.ts | 6 +++--- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 08665f8d2e..6964b68204 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -7,6 +7,7 @@ import { toast } from "react-toastify"; import Loader from "../components/Loader"; import { SUCCESS_LOG_OUT } from "../utils/constants"; import { User } from "../types/types"; +import { getToken, removeToken, setToken } from "../utils/token"; type AuthContextType = { signup: ( @@ -32,11 +33,11 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const navigate = useNavigate(); useEffect(() => { - const accessToken = localStorage.getItem("token"); + const accessToken = getToken(); if (accessToken) { userClient .get("/auth/verify-token", { - headers: { Authorization: `Bearer ${accessToken}` }, + headers: { Authorization: accessToken }, }) .then((res) => setUser(res.data.data)) .catch(() => setUser(null)) @@ -82,7 +83,7 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }) .then((res) => { const { accessToken, user } = res.data.data; - localStorage.setItem("token", accessToken); + setToken(accessToken); setUser(user); navigate("/home"); }) @@ -96,7 +97,7 @@ const AuthProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const logout = () => { - localStorage.removeItem("token"); + removeToken(); setUser(null); navigate("/"); toast.success(SUCCESS_LOG_OUT); diff --git a/frontend/src/contexts/ProfileContext.tsx b/frontend/src/contexts/ProfileContext.tsx index cafed8b55d..ecc40069fd 100644 --- a/frontend/src/contexts/ProfileContext.tsx +++ b/frontend/src/contexts/ProfileContext.tsx @@ -10,6 +10,7 @@ import { } from "../utils/constants"; import { toast } from "react-toastify"; import axios from "axios"; +import { getToken } from "../utils/token"; interface UserProfileBase { firstName: string; @@ -81,10 +82,10 @@ const ProfileContextProvider: React.FC<{ children: React.ReactNode }> = ({ }; const updateProfile = async (data: UserProfileBase): Promise => { - const token = localStorage.getItem("token"); + const token = getToken(); try { const res = await userClient.patch(`/users/${user?.id}`, data, { - headers: { Authorization: `Bearer ${token}` }, + headers: { Authorization: token }, }); setUser(res.data.data); toast.success(SUCCESS_PROFILE_UPDATE_MESSAGE); @@ -110,12 +111,12 @@ const ProfileContextProvider: React.FC<{ children: React.ReactNode }> = ({ oldPassword: string; newPassword: string; }) => { - const token = localStorage.getItem("token"); + const token = getToken(); await userClient .patch( `/users/${user?.id}`, { oldPassword, newPassword }, - { headers: { Authorization: `Bearer ${token}` } } + { headers: { Authorization: token } } ) .then(() => toast.success(SUCCESS_PW_UPDATE_MESSAGE)) .catch((err) => { diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts index 1300e8dbd2..7d3009310a 100644 --- a/frontend/src/reducers/questionReducer.ts +++ b/frontend/src/reducers/questionReducer.ts @@ -1,6 +1,7 @@ import { Dispatch } from "react"; import { questionClient } from "../utils/api"; import { isString, isStringArray } from "../utils/typeChecker"; +import { getToken } from "../utils/token"; type TestcaseFiles = { testcaseInputFile: File | null; @@ -126,11 +127,11 @@ export const uploadTestcaseFiles = async ( formData.append("requestType", requestType); try { - const accessToken = localStorage.getItem("token"); + const accessToken = getToken(); const res = await questionClient.post("/tcfiles", formData, { headers: { "Content-Type": "multipart/form-data", - Authorization: `Bearer ${accessToken}`, + Authorization: accessToken, }, }); return res.data; @@ -159,7 +160,7 @@ export const createQuestion = async ( const { testcaseInputFileUrl, testcaseOutputFileUrl } = uploadResult.urls; - const accessToken = localStorage.getItem("token"); + const accessToken = getToken(); return questionClient .post( "/", @@ -176,7 +177,7 @@ export const createQuestion = async ( }, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: accessToken, }, } ) @@ -297,7 +298,7 @@ export const updateQuestionById = async ( }; } - const accessToken = localStorage.getItem("token"); + const accessToken = getToken(); return questionClient .put( `/${questionId}`, @@ -315,7 +316,7 @@ export const updateQuestionById = async ( }, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: accessToken, }, } ) @@ -337,10 +338,10 @@ export const updateQuestionById = async ( export const deleteQuestionById = async (questionId: string) => { try { - const accessToken = localStorage.getItem("token"); + const accessToken = getToken(); await questionClient.delete(`/${questionId}`, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: accessToken, }, }); return true; @@ -363,11 +364,11 @@ export const createImageUrls = async ( formData: FormData ): Promise<{ imageUrls: string[]; message: string } | null> => { try { - const accessToken = localStorage.getItem("token"); + const accessToken = getToken(); const response = await questionClient.post("/images", formData, { headers: { "Content-Type": "multipart/form-data", - Authorization: `Bearer ${accessToken}`, + Authorization: accessToken, }, }); return response.data; diff --git a/frontend/src/utils/token.ts b/frontend/src/utils/token.ts index 11998b856b..900f18144a 100644 --- a/frontend/src/utils/token.ts +++ b/frontend/src/utils/token.ts @@ -1,11 +1,11 @@ export const setToken = (token: string) => { - const bearerToken = `Bearer ${token}`; - localStorage.setItem("accessToken", bearerToken); + localStorage.setItem("accessToken", token); }; export const getToken = () => { const token = localStorage.getItem("accessToken"); - return token; + const bearerToken = `Bearer ${token}`; + return bearerToken; }; export const removeToken = () => { From f9498b03e1d7c879a92e11c5c6aabfb2100ff269 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:39:35 +0800 Subject: [PATCH 144/192] Resolve comment issues -fix code execution test bug -update tests and swagger for qn history -return bearer token only if token exists, else auth context useeffect will run even when token does not exist since accesstoken still returns a non falsy value --- .../tests/codeExecutionRoutes.spec.ts | 2 +- backend/qn-history-service/swagger.yml | 14 ++++++++++++-- .../tests/qnHistoryRoutes.spec.ts | 7 +++++++ frontend/src/components/CodeEditor/index.tsx | 2 +- frontend/src/reducers/qnHistoryReducer.ts | 4 ++-- frontend/src/utils/sessionTime.ts | 2 -- frontend/src/utils/token.ts | 8 ++++++-- 7 files changed, 29 insertions(+), 10 deletions(-) diff --git a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts index e1ab51161f..81996aef95 100644 --- a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts +++ b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts @@ -90,7 +90,7 @@ describe("Code execution routes", () => { expect(response.body.data).toBeInstanceOf(Array); expect(response.body.data[0]).toHaveProperty("isMatch", true); expect(response.body.data[0]["isMatch"]).toBe(true); - expect(response.body.data[1]["isMatch"]).toBe(false); + expect(response.body.data[1]["isMatch"]).toBe(true); }); }); }); diff --git a/backend/qn-history-service/swagger.yml b/backend/qn-history-service/swagger.yml index ab684a877f..b94f46e9e4 100644 --- a/backend/qn-history-service/swagger.yml +++ b/backend/qn-history-service/swagger.yml @@ -5,6 +5,12 @@ info: version: 1.0.0 components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: QnHistory: properties: @@ -28,7 +34,7 @@ components: description: Date that question was attempted timeTaken: type: number - description: Time taken for question attempt in minutes + description: Time taken for question attempt in seconds code: type: string description: Code submitted @@ -63,7 +69,7 @@ definitions: description: Date that question was attempted timeTaken: type: number - description: Time taken for question attempt in minutes + description: Time taken for question attempt in seconds code: type: string description: Code submitted @@ -119,6 +125,8 @@ paths: - qnhistories summary: Creates a question history description: Creates a question history + security: + - bearerAuth: [] requestBody: required: true content: @@ -218,6 +226,8 @@ paths: - qnhistories summary: Updates a question history description: Updates a question history + security: + - bearerAuth: [] parameters: - in: path name: id diff --git a/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts b/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts index 303dc789a5..be22f9ef03 100644 --- a/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts +++ b/backend/qn-history-service/tests/qnHistoryRoutes.spec.ts @@ -1,3 +1,4 @@ +import { NextFunction, Request, Response } from "express"; import { faker } from "@faker-js/faker"; import supertest from "supertest"; import app from "../src/app"; @@ -16,6 +17,12 @@ const BASE_URL = "/api/qnhistories"; faker.seed(0); +jest.mock("../src/middlewares/basicAccessControl", () => ({ + verifyToken: jest.fn((res: Request, req: Response, next: NextFunction) => + next() + ), +})); + describe("Qn History Routes", () => { describe("POST / ", () => { it("Creates new qn history", async () => { diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index d8df708837..3fb1b3678f 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -75,8 +75,8 @@ const CodeEditor: React.FC = (props) => { const loadTemplate = async () => { if (matchUser && partner && matchCriteria && questionId && questionTitle) { - await initDocument(uid, roomId, template, matchUser.id, partner.id, matchCriteria.language, questionId, questionTitle); checkDocReady(); + await initDocument(uid, roomId, template, matchUser.id, partner.id, matchCriteria.language, questionId, questionTitle); setIsDocumentLoaded(true); } }; diff --git a/frontend/src/reducers/qnHistoryReducer.ts b/frontend/src/reducers/qnHistoryReducer.ts index 78fd0d4635..f45f0ed866 100644 --- a/frontend/src/reducers/qnHistoryReducer.ts +++ b/frontend/src/reducers/qnHistoryReducer.ts @@ -1,6 +1,7 @@ import { Dispatch } from "react"; import { qnHistoryClient } from "../utils/api"; import { isString, isStringArray } from "../utils/typeChecker"; +import { getToken } from "../utils/token"; type QnHistoryDetail = { id: string; @@ -151,7 +152,6 @@ export const updateQnHistoryById = async ( >, dispatch: Dispatch ): Promise => { - const accessToken = localStorage.getItem("token"); return qnHistoryClient .put( `/${qnHistoryId}`, @@ -163,7 +163,7 @@ export const updateQnHistoryById = async ( }, { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: getToken(), }, } ) diff --git a/frontend/src/utils/sessionTime.ts b/frontend/src/utils/sessionTime.ts index a769d2d9ca..72ace6bfbf 100644 --- a/frontend/src/utils/sessionTime.ts +++ b/frontend/src/utils/sessionTime.ts @@ -5,8 +5,6 @@ export const extractMinutesFromTime = (time: number) => export const extractSecondsFromTime = (time: number) => time % 60; // after extracting hours and minutes -export const extractMinutesOnly = (time: number) => time / 60; - export const convertDateString = (date: string): string => { const convertedDate = new Date(date); const dateString = convertedDate.toLocaleDateString("en-GB", { diff --git a/frontend/src/utils/token.ts b/frontend/src/utils/token.ts index 900f18144a..2b3e328bf4 100644 --- a/frontend/src/utils/token.ts +++ b/frontend/src/utils/token.ts @@ -4,8 +4,12 @@ export const setToken = (token: string) => { export const getToken = () => { const token = localStorage.getItem("accessToken"); - const bearerToken = `Bearer ${token}`; - return bearerToken; + if (token) { + const bearerToken = `Bearer ${token}`; + return bearerToken; + } else { + return null; + } }; export const removeToken = () => { From be86ef94f9832d8c100dbbfa467703837c4904cb Mon Sep 17 00:00:00 2001 From: jolynloh Date: Fri, 8 Nov 2024 20:58:21 +0800 Subject: [PATCH 145/192] Config and edits --- .../question-service/src/middlewares/basicAccessControl.ts | 2 +- backend/question-service/src/server.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/question-service/src/middlewares/basicAccessControl.ts b/backend/question-service/src/middlewares/basicAccessControl.ts index c0830885fc..b468e5ea50 100644 --- a/backend/question-service/src/middlewares/basicAccessControl.ts +++ b/backend/question-service/src/middlewares/basicAccessControl.ts @@ -12,6 +12,6 @@ export const verifyAdminToken = ( .then(() => next()) .catch((err) => { console.log(err.response); - return res.status(err.response.status).json(err.response.data); + return res.status(err.response?.status).json(err.response?.data); }); }; diff --git a/backend/question-service/src/server.ts b/backend/question-service/src/server.ts index 513945f03e..94972d8782 100644 --- a/backend/question-service/src/server.ts +++ b/backend/question-service/src/server.ts @@ -1,5 +1,6 @@ import app from "./app.ts"; import connectDB from "./config/db.ts"; +import { seedQuestions } from "./scripts/seed.ts"; const PORT = process.env.SERVICE_PORT || 3000; @@ -7,12 +8,15 @@ if (process.env.NODE_ENV !== "test") { connectDB() .then(() => { console.log("MongoDB Connected!"); + // seedQuestions(); - app.listen(PORT, () => { + const server = app.listen(PORT, () => { console.log( `Question service server listening on http://localhost:${PORT}`, ); }); + + server.keepAliveTimeout = 70 * 1000; // set timeout value to > load balancer idle timeout (60s) }) .catch((err) => { console.error("Failed to connect to DB"); From 3132abd6e615da748f2a6ae085c3e3ccab8ab3bf Mon Sep 17 00:00:00 2001 From: jolynloh Date: Fri, 8 Nov 2024 21:03:26 +0800 Subject: [PATCH 146/192] Remove unused files --- .ebextensions/01-launch-template.config | 4 ---- .gitignore | 5 ----- 2 files changed, 9 deletions(-) delete mode 100644 .ebextensions/01-launch-template.config diff --git a/.ebextensions/01-launch-template.config b/.ebextensions/01-launch-template.config deleted file mode 100644 index d2e4f5cdc9..0000000000 --- a/.ebextensions/01-launch-template.config +++ /dev/null @@ -1,4 +0,0 @@ -option_settings: - aws:autoscaling:launchconfiguration: - RootVolumeType: gp3 - DisableIMDSv1: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index ff2ce92680..84f98a5642 100644 --- a/.gitignore +++ b/.gitignore @@ -29,8 +29,3 @@ coverage # Environment files .env - -# Elastic Beanstalk Files -.elasticbeanstalk/* -!.elasticbeanstalk/*.cfg.yml -!.elasticbeanstalk/*.global.yml From 53c6a238b44c934c88ffde59bbb53fdcaf23bea6 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Fri, 8 Nov 2024 21:04:43 +0800 Subject: [PATCH 147/192] Comment out unused import --- backend/question-service/src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/question-service/src/server.ts b/backend/question-service/src/server.ts index 94972d8782..ef42c5463d 100644 --- a/backend/question-service/src/server.ts +++ b/backend/question-service/src/server.ts @@ -1,6 +1,6 @@ import app from "./app.ts"; import connectDB from "./config/db.ts"; -import { seedQuestions } from "./scripts/seed.ts"; +// import { seedQuestions } from "./scripts/seed.ts"; const PORT = process.env.SERVICE_PORT || 3000; From 1c043dee4974af44f8b86ba33e6ff21f2fc68b13 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Fri, 8 Nov 2024 21:06:16 +0800 Subject: [PATCH 148/192] Remove console log... --- frontend/src/utils/matchSocket.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/utils/matchSocket.ts b/frontend/src/utils/matchSocket.ts index bbae84ffa6..74039afad3 100644 --- a/frontend/src/utils/matchSocket.ts +++ b/frontend/src/utils/matchSocket.ts @@ -3,8 +3,6 @@ import { io } from "socket.io-client"; const MATCH_SOCKET_URL = import.meta.env.VITE_MATCH_SERVICE_URL ?? "http://localhost:3002"; -console.log(import.meta.env.VITE_MATCH_SERVICE_URL); - export const matchSocket = io(MATCH_SOCKET_URL, { reconnectionAttempts: 3, autoConnect: false, From 01fc14b36da39ccedd895183c6259e30f3cbc4d0 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Fri, 8 Nov 2024 22:01:22 +0800 Subject: [PATCH 149/192] Fix frontend testcases --- .../QuestionImageContainer/QuestionImageContainer.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx b/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx index 317628bbba..599c6cda4c 100644 --- a/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx +++ b/frontend/src/components/QuestionImageContainer/QuestionImageContainer.test.tsx @@ -10,7 +10,7 @@ jest.mock("../../utils/api", () => ({ describe("Question Image Container", () => { const mockLocalStorage = (() => { - const store: { [key: string]: string } = { token: "test" }; + const store: { [key: string]: string } = { accessToken: "test" }; return { getItem(key: string) { @@ -122,7 +122,7 @@ describe("Question Image Container", () => { expect.any(FormData), expect.objectContaining({ headers: { - Authorization: `Bearer ${mockLocalStorage.getItem("token")}`, + Authorization: `Bearer ${mockLocalStorage.getItem("accessToken")}`, "Content-Type": "multipart/form-data", }, }) From fd2a2d228fd76a8e76db0c044e7dfafde7cbd7db Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Fri, 8 Nov 2024 23:16:56 +0800 Subject: [PATCH 150/192] fix linting error --- .../src/handlers/websocketHandler.ts | 35 +++++++++---------- frontend/src/contexts/MatchContext.tsx | 15 +------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index a2065f7bbc..92f9861836 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -119,27 +119,24 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { userConnections.delete(uid); }); - socket.on( - MatchEvents.MATCH_ACCEPT_REQUEST, - (matchId: string, userId1: string, userId2: string) => { - const partnerAccepted = handleMatchAccept(matchId); - if (partnerAccepted) { - const match = getMatchById(matchId); - if (!match) { - return; - } - - const { complexity, category } = match; - getRandomQuestion(complexity, category).then((res) => { - io.to(matchId).emit( - MatchEvents.MATCH_SUCCESSFUL, - res.data.question.id, - res.data.question.title - ); - }); + socket.on(MatchEvents.MATCH_ACCEPT_REQUEST, (matchId: string) => { + const partnerAccepted = handleMatchAccept(matchId); + if (partnerAccepted) { + const match = getMatchById(matchId); + if (!match) { + return; } + + const { complexity, category } = match; + getRandomQuestion(complexity, category).then((res) => { + io.to(matchId).emit( + MatchEvents.MATCH_SUCCESSFUL, + res.data.question.id, + res.data.question.title + ); + }); } - ); + }); socket.on( MatchEvents.MATCH_DECLINE_REQUEST, diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index b8293ee381..c97f15f46d 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -17,9 +17,6 @@ import useAppNavigate from "../hooks/useAppNavigate"; import { UNSAFE_NavigationContext } from "react-router-dom"; import { Action, type History, type Transition } from "history"; -let matchUserId: string; -let partnerUserId: string; - type MatchUser = { id: string; username: string; @@ -126,10 +123,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { username: user.username, profile: user.profilePictureUrl, }); - matchUserId = user.id; } else { setMatchUser(null); - matchUserId = ""; } }, [user]); @@ -183,7 +178,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } setMatchId(null); setPartner(null); - partnerUserId = ""; setMatchPending(false); setLoading(false); }; @@ -307,10 +301,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setMatchId(matchId); if (matchUser?.id === user1.id) { setPartner(user2); - partnerUserId = user2.id; } else { setPartner(user1); - partnerUserId = user1.id; } setMatchPending(true); appNavigate(MatchPaths.MATCHED); @@ -391,9 +383,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const acceptMatch = () => { matchSocket.emit( MatchEvents.MATCH_ACCEPT_REQUEST, - matchId, - matchUserId, - partnerUserId + matchId ); }; @@ -429,7 +419,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { if (requested) { appNavigate(MatchPaths.MATCHING); setPartner(null); - partnerUserId = ""; } } ); @@ -479,11 +468,9 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { if (match) { setMatchId(match.matchId); setPartner(match.partner); - partnerUserId = match.partner.id; } else { setMatchId(null); setPartner(null); - partnerUserId = ""; } setLoading(false); } From 5e939fd49ea65ada6bf9b2095304bded8cca6561 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Fri, 8 Nov 2024 23:33:34 +0800 Subject: [PATCH 151/192] Fix excessive re-rendering of components that use collab context --- .../src/handlers/websocketHandler.ts | 4 +- .../CollabSessionControls/index.tsx | 119 +++++++++++++++++- frontend/src/contexts/CollabContext.tsx | 75 +++-------- frontend/src/pages/CollabSandbox/index.tsx | 41 +----- 4 files changed, 134 insertions(+), 105 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 402e36131b..c1c43d511c 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -115,8 +115,8 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { socket.on( CollabEvents.END_SESSION_REQUEST, - (roomId: string, timeTaken: number) => { - socket.to(roomId).emit(CollabEvents.END_SESSION, timeTaken); + (roomId: string, sessionDuration: number) => { + socket.to(roomId).emit(CollabEvents.END_SESSION, sessionDuration); } ); diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 81b30bdb6a..4fcf0b0671 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -1,15 +1,99 @@ import { Button, Stack } from "@mui/material"; import Stopwatch from "../Stopwatch"; import { useCollab } from "../../contexts/CollabContext"; -import { USE_COLLAB_ERROR_MESSAGE } from "../../utils/constants"; +import { + COLLAB_ENDED_MESSAGE, + COLLAB_PARTNER_DISCONNECTED_MESSAGE, + USE_COLLAB_ERROR_MESSAGE, + USE_MATCH_ERROR_MESSAGE, +} from "../../utils/constants"; +import { useEffect, useReducer, useState } from "react"; +import CustomDialog from "../CustomDialog"; +import { + extractMinutesFromTime, + extractSecondsFromTime, +} from "../../utils/sessionTime"; +import { CollabEvents, collabSocket } from "../../utils/collabSocket"; +import { toast } from "react-toastify"; +import reducer, { + getQuestionById, + initialState, +} from "../../reducers/questionReducer"; +import { useMatch } from "../../contexts/MatchContext"; const CollabSessionControls: React.FC = () => { + const match = useMatch(); + if (!match) { + throw new Error(USE_MATCH_ERROR_MESSAGE); + } + + const { questionId } = match; + const collab = useCollab(); if (!collab) { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { handleSubmitSessionClick, handleEndSessionClick, time } = collab; + const { + handleSubmitSessionClick, + handleEndSessionClick, + handleConfirmEndSession, + isEndSessionModalOpen, + handleRejectEndSession, + handleExitSession, + isExitSessionModalOpen, + } = collab; + + const [time, setTime] = useState(0); + const [stopTime, setStopTime] = useState(false); + + const [state, dispatch] = useReducer(reducer, initialState); + const { selectedQuestion } = state; + + useEffect(() => { + collabSocket.once(CollabEvents.END_SESSION, (sessionDuration: number) => { + collabSocket.off(CollabEvents.PARTNER_DISCONNECTED); + toast.info(COLLAB_ENDED_MESSAGE); + handleConfirmEndSession( + time, + setTime, + setStopTime, + true, + sessionDuration + ); + }); + + collabSocket.once(CollabEvents.PARTNER_DISCONNECTED, () => { + collabSocket.off(CollabEvents.END_SESSION); + toast.error(COLLAB_PARTNER_DISCONNECTED_MESSAGE); + handleConfirmEndSession(time, setTime, setStopTime, true); + }); + + return () => { + collabSocket.off(CollabEvents.END_SESSION); + collabSocket.off(CollabEvents.PARTNER_DISCONNECTED); + }; + }, []); + + useEffect(() => { + if (stopTime) { + return; + } + + const intervalId = setInterval( + () => setTime((prevTime) => prevTime + 1), + 1000 + ); + + return () => clearInterval(intervalId); + }, [time, stopTime]); + + useEffect(() => { + if (!questionId) { + return; + } + getQuestionById(questionId, dispatch); + }, [questionId]); return ( @@ -21,7 +105,7 @@ const CollabSessionControls: React.FC = () => { }} variant="outlined" color="success" - onClick={() => handleSubmitSessionClick()} + onClick={() => handleSubmitSessionClick(time)} > Submit @@ -38,6 +122,35 @@ const CollabSessionControls: React.FC = () => { > End Session + + Are you sure you want to end the collaboration session? +
+ You will not be able to rejoin. + + } + primaryAction="Confirm" + handlePrimaryAction={() => + handleConfirmEndSession(time, setTime, setStopTime, false) + } + secondaryAction="Cancel" + open={isEndSessionModalOpen} + handleClose={handleRejectEndSession} + /> +
); }; diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index d59691fbf9..e81487b71a 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -1,19 +1,11 @@ /* eslint-disable react-refresh/only-export-components */ -import React, { - createContext, - useContext, - useEffect, - useRef, - useState, -} from "react"; +import React, { createContext, useContext, useState } from "react"; import { USE_MATCH_ERROR_MESSAGE, FAILED_TESTCASE_MESSAGE, SUCCESS_TESTCASE_MESSAGE, FAILED_TO_SUBMIT_CODE_MESSAGE, - COLLAB_PARTNER_DISCONNECTED_MESSAGE, - COLLAB_ENDED_MESSAGE, COLLAB_END_ERROR, } from "../utils/constants"; import { toast } from "react-toastify"; @@ -44,19 +36,20 @@ export type CompilerResult = { }; type CollabContextType = { - handleSubmitSessionClick: () => void; + handleSubmitSessionClick: (time: number) => void; handleEndSessionClick: () => void; handleRejectEndSession: () => void; handleConfirmEndSession: ( + time: number, + setTime: React.Dispatch>, + setStopTime: React.Dispatch>, isInitiatedByPartner: boolean, - time?: number + sessionDuration?: number ) => void; - checkPartnerStatus: () => void; setCode: React.Dispatch>; compilerResult: CompilerResult[]; setCompilerResult: React.Dispatch>; isEndSessionModalOpen: boolean; - time: number; resetCollab: () => void; handleExitSession: () => void; isExitSessionModalOpen: boolean; @@ -94,31 +87,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { useState(false); const [isExitSessionModalOpen, setIsExitSessionModalOpen] = useState(false); - const [time, setTime] = useState(0); - const [stopTime, setStopTime] = useState(true); - const timeRef = useRef(time); - const codeRef = useRef(code); - - useEffect(() => { - timeRef.current = time; - codeRef.current = code; - }, [time, code]); - - useEffect(() => { - if (stopTime) { - return; - } - - const intervalId = setInterval( - () => setTime((prevTime) => prevTime + 1), - 1000 - ); - - return () => clearInterval(intervalId); - }, [time, stopTime]); - - const handleSubmitSessionClick = async () => { + const handleSubmitSessionClick = async (time: number) => { try { const res = await codeExecutionClient.post("/", { questionId, @@ -167,8 +137,11 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const handleConfirmEndSession = async ( + time: number, + setTime: React.Dispatch>, + setStopTime: React.Dispatch>, isInitiatedByPartner: boolean, - timeTaken?: number + sessionDuration?: number ) => { setIsEndSessionModalOpen(false); @@ -181,8 +154,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setStopTime(true); setIsExitSessionModalOpen(true); - if (isInitiatedByPartner && timeTaken) { - setTime(timeTaken); + if (isInitiatedByPartner && sessionDuration) { + setTime(sessionDuration); } else { // Get question history const data = await qnHistoryClient.get(qnHistoryId); @@ -194,8 +167,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { { submissionStatus: "Attempted", dateAttempted: new Date().toISOString(), - timeTaken: timeRef.current, - code: codeRef.current.replace(/\t/g, " ".repeat(4)), + timeTaken: time, + code: code.replace(/\t/g, " ".repeat(4)), }, qnHistoryDispatch ); @@ -225,24 +198,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { resetCollab(); }; - const checkPartnerStatus = () => { - collabSocket.once(CollabEvents.END_SESSION, (timeTaken: number) => { - collabSocket.off(CollabEvents.PARTNER_DISCONNECTED); - toast.info(COLLAB_ENDED_MESSAGE); - handleConfirmEndSession(true, timeTaken); - }); - - collabSocket.once(CollabEvents.PARTNER_DISCONNECTED, () => { - collabSocket.off(CollabEvents.END_SESSION); - toast.error(COLLAB_PARTNER_DISCONNECTED_MESSAGE); - handleConfirmEndSession(true); - }); - }; - const resetCollab = () => { setCompilerResult([]); - setTime(0); - setStopTime(false); }; return ( @@ -252,12 +209,10 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { handleEndSessionClick, handleRejectEndSession, handleConfirmEndSession, - checkPartnerStatus, setCode, compilerResult, setCompilerResult, isEndSessionModalOpen, - time, resetCollab, handleExitSession, isExitSessionModalOpen, diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index a334654428..7fa0fbcb2a 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -22,11 +22,6 @@ import TestCase from "../../components/TestCase"; import CodeEditor from "../../components/CodeEditor"; import { CollabSessionData, join, leave } from "../../utils/collabSocket"; import { toast } from "react-toastify"; -import CustomDialog from "../../components/CustomDialog"; -import { - extractMinutesFromTime, - extractSecondsFromTime, -} from "../../utils/sessionTime"; const CollabSandbox: React.FC = () => { const match = useMatch(); @@ -41,17 +36,7 @@ const CollabSandbox: React.FC = () => { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { - compilerResult, - handleRejectEndSession, - handleConfirmEndSession, - checkPartnerStatus, - isEndSessionModalOpen, - resetCollab, - handleExitSession, - isExitSessionModalOpen, - time, - } = collab; + const { compilerResult, resetCollab } = collab; const [state, dispatch] = useReducer(reducer, initialState); const { selectedQuestion } = state; @@ -75,7 +60,6 @@ const CollabSandbox: React.FC = () => { const editorState = await join(matchUser.id, matchId); if (editorState.ready) { setEditorState(editorState); - checkPartnerStatus(); } else { toast.error(COLLAB_CONNECTION_ERROR); setIsConnecting(false); @@ -119,29 +103,6 @@ const CollabSandbox: React.FC = () => { return ( - - Are you sure you want to end the collaboration session? -
- You will not be able to rejoin. - - } - primaryAction="Confirm" - handlePrimaryAction={() => handleConfirmEndSession(false)} - secondaryAction="Cancel" - open={isEndSessionModalOpen} - handleClose={handleRejectEndSession} - /> - Date: Fri, 8 Nov 2024 23:33:48 +0800 Subject: [PATCH 152/192] Preview test case and code template --- .../QuestionCodeTemplates/index.tsx | 78 ++++++++----------- .../QuestionDetail/QuestionDetail.test.tsx | 36 +++++++++ .../src/components/QuestionDetail/index.tsx | 72 ++++++++++++++++- frontend/src/pages/CollabSandbox/index.tsx | 12 ++- frontend/src/pages/NewQuestion/index.tsx | 63 ++++++++++++++- frontend/src/pages/QuestionDetail/index.tsx | 7 ++ frontend/src/pages/QuestionEdit/index.tsx | 31 +++++++- frontend/src/reducers/questionReducer.ts | 13 +--- 8 files changed, 249 insertions(+), 63 deletions(-) diff --git a/frontend/src/components/QuestionCodeTemplates/index.tsx b/frontend/src/components/QuestionCodeTemplates/index.tsx index b5ee81fa3b..da36eb0120 100644 --- a/frontend/src/components/QuestionCodeTemplates/index.tsx +++ b/frontend/src/components/QuestionCodeTemplates/index.tsx @@ -3,12 +3,16 @@ import { Box, IconButton, Stack, - TextField, ToggleButton, ToggleButtonGroup, Tooltip, Typography, } from "@mui/material"; +import CodeMirror from "@uiw/react-codemirror"; +import { EditorView } from "@codemirror/view"; +import { indentUnit } from "@codemirror/language"; +import { langs } from "@uiw/codemirror-extensions-langs"; +import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; import { useState } from "react"; import { CODE_TEMPLATES_TOOLTIP_MESSAGE } from "../../utils/constants"; @@ -16,16 +20,24 @@ interface QuestionCodeTemplatesProps { codeTemplates: { [key: string]: string; }; - setCodeTemplates: React.Dispatch< + setCodeTemplates?: React.Dispatch< React.SetStateAction<{ [key: string]: string; }> >; + isEditable: boolean; } +const languageSupport = { + python: langs.python(), + java: langs.java(), + c: langs.c(), +}; + const QuestionCodeTemplates: React.FC = ({ codeTemplates, setCodeTemplates, + isEditable, }) => { const [selectedLanguage, setSelectedLanguage] = useState("python"); @@ -38,32 +50,12 @@ const QuestionCodeTemplates: React.FC = ({ } }; - const handleCodeChange = (event: React.ChangeEvent) => { - const { value } = event.target; - setCodeTemplates((prevTemplates) => ({ - ...prevTemplates, - [selectedLanguage]: value, - })); - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleTabKeys = (event: any) => { - const { value } = event.target; - - if (event.key === "Tab") { - event.preventDefault(); - - const cursorPosition = event.target.selectionStart; - const cursorEndPosition = event.target.selectionEnd; - const tab = "\t"; - - event.target.value = - value.substring(0, cursorPosition) + - tab + - value.substring(cursorEndPosition); - - event.target.selectionStart = cursorPosition + 1; - event.target.selectionEnd = cursorPosition + 1; + const handleCodeChange = (value: string) => { + if (setCodeTemplates) { + setCodeTemplates((prevTemplates) => ({ + ...prevTemplates, + [selectedLanguage]: value, + })); } }; @@ -104,27 +96,19 @@ const QuestionCodeTemplates: React.FC = ({ C -
); diff --git a/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx b/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx index 02e8ba433a..0d4a493ec2 100644 --- a/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx +++ b/frontend/src/components/QuestionDetail/QuestionDetail.test.tsx @@ -18,12 +18,24 @@ describe("Question details", () => { const complexity = "Easy"; const categories = ["Algorithms", "Data Structures"]; const description = "# Test description"; + const pythonTemplate = "Python template"; + const javaTemplate = "Java template"; + const cTemplate = "C template"; + const inputTestCases = ["1", "2"]; + const outputTestCases = ["1", "2"]; render( ); expect(screen.getByText(title)).toBeInTheDocument(); @@ -34,12 +46,24 @@ describe("Question details", () => { const complexity = "Easy"; const categories = ["Algorithms", "Data Structures"]; const description = "# Test description"; + const pythonTemplate = "Python template"; + const javaTemplate = "Java template"; + const cTemplate = "C template"; + const inputTestCases = ["1", "2"]; + const outputTestCases = ["1", "2"]; render( ); expect(screen.getByText(complexity)).toBeInTheDocument(); @@ -50,12 +74,24 @@ describe("Question details", () => { const complexity = "Easy"; const categories = ["Algorithms", "Data Structures"]; const description = "# Test description"; + const pythonTemplate = "Python template"; + const javaTemplate = "Java template"; + const cTemplate = "C template"; + const inputTestCases = ["1", "2"]; + const outputTestCases = ["1", "2"]; render( ); expect(screen.getByText(categories[0])).toBeInTheDocument(); diff --git a/frontend/src/components/QuestionDetail/index.tsx b/frontend/src/components/QuestionDetail/index.tsx index 6d8562fed0..8929435d2b 100644 --- a/frontend/src/components/QuestionDetail/index.tsx +++ b/frontend/src/components/QuestionDetail/index.tsx @@ -1,18 +1,53 @@ -import { Box, Chip, List, ListItem, Stack, Typography } from "@mui/material"; +import { + Box, + Chip, + List, + ListItem, + Stack, + Typography, + styled, +} from "@mui/material"; import MDEditor from "@uiw/react-md-editor"; +import QuestionCodeTemplates from "../QuestionCodeTemplates"; +import theme from "../../theme"; interface QuestionDetailProps { title: string; complexity: string | null; categories: string[]; description: string; + cTemplate: string; + javaTemplate: string; + pythonTemplate: string; + inputTestCases: string[]; + outputTestCases: string[]; + showCodeTemplate: boolean; + showTestCases: boolean; } +const StyledBox = styled(Box)(({ theme }) => ({ + margin: theme.spacing(2, 0), +})); + +const StyledTypography = styled(Typography)(({ theme }) => ({ + background: theme.palette.divider, + padding: theme.spacing(1, 2), + borderRadius: theme.spacing(1), + whiteSpace: "pre-line", +})); + const QuestionDetail: React.FC = ({ title, complexity, categories, description, + cTemplate, + javaTemplate, + pythonTemplate, + inputTestCases, + outputTestCases, + showCodeTemplate, + showTestCases, }) => { return ( = ({ }} /> + + {showTestCases && ( + + Test Cases + {Array.from({ + length: Math.max(inputTestCases.length, outputTestCases.length), + }).map((_, index) => ( + + + Input + + {inputTestCases[index] || "\u00A0"} + + + + Output + + {outputTestCases[index] || "\u00A0"} + + + + ))} + + )} + + {showCodeTemplate && ( + + )} ); }; diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index c2c96abcc4..cfcf4ccac5 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -179,12 +179,22 @@ const CollabSandbox: React.FC = () => { - + => { + if (!file) return []; + + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + + fileReader.onload = (e) => { + const fileContent = (e.target?.result as string) || ""; + resolve(fileContent.replace(/\r\n/g, "\n").split("\n\n")); + }; + + fileReader.onerror = () => { + reject(new Error("Error reading file")); + }; + + fileReader.readAsText(file); + }); +}; + const NewQuestion = () => { const navigate = useNavigate(); @@ -51,6 +72,9 @@ const NewQuestion = () => { null ); + const [inputTestCases, setInputTestCases] = useState([]); + const [outputTestCases, setOutputTestCases] = useState([]); + const [codeTemplates, setCodeTemplates] = useState<{ [key: string]: string }>( { python: PYTHON_CODE_TEMPLATE, @@ -59,12 +83,32 @@ const NewQuestion = () => { } ); + useEffect(() => { + const loadTestCases = async () => { + if (testcaseInputFile) { + setInputTestCases(await convertFileToTestCaseFormat(testcaseInputFile)); + } + if (testcaseOutputFile) { + setOutputTestCases( + await convertFileToTestCaseFormat(testcaseOutputFile) + ); + } + }; + + loadTestCases(); + }, [testcaseInputFile, testcaseOutputFile]); + const handleBack = () => { if ( title || markdownText || selectedComplexity || - selectedCategories.length > 0 + selectedCategories.length > 0 || + testcaseInputFile || + testcaseOutputFile || + codeTemplates.python !== PYTHON_CODE_TEMPLATE || + codeTemplates.java !== JAVA_CODE_TEMPLATE || + codeTemplates.c !== C_CODE_TEMPLATE ) { if (!confirm(ABORT_CREATE_OR_EDIT_QUESTION_CONFIRMATION_MESSAGE)) { return; @@ -124,6 +168,13 @@ const NewQuestion = () => { complexity={selectedComplexity} categories={selectedCategories} description={markdownText} + cTemplate={codeTemplates.c} + javaTemplate={codeTemplates.java} + pythonTemplate={codeTemplates.python} + inputTestCases={inputTestCases} + outputTestCases={outputTestCases} + showCodeTemplate={true} + showTestCases={true} /> ) : ( <> @@ -173,6 +224,7 @@ const NewQuestion = () => { )} @@ -189,7 +241,12 @@ const NewQuestion = () => { !title && !markdownText && !selectedComplexity && - selectedCategories.length === 0 + selectedCategories.length === 0 && + !testcaseInputFile && + !testcaseOutputFile && + !(codeTemplates.python === PYTHON_CODE_TEMPLATE) && + !(codeTemplates.java === JAVA_CODE_TEMPLATE) && + !(codeTemplates.c === C_CODE_TEMPLATE) } onClick={() => setIsPreviewQuestion((prev) => !prev)} > diff --git a/frontend/src/pages/QuestionDetail/index.tsx b/frontend/src/pages/QuestionDetail/index.tsx index 3cb1babf73..a3af1a8d35 100644 --- a/frontend/src/pages/QuestionDetail/index.tsx +++ b/frontend/src/pages/QuestionDetail/index.tsx @@ -43,6 +43,13 @@ const QuestionDetail: React.FC = () => { complexity={state.selectedQuestion.complexity} categories={state.selectedQuestion.categories} description={state.selectedQuestion.description} + cTemplate={state.selectedQuestion.cTemplate} + javaTemplate={state.selectedQuestion.javaTemplate} + pythonTemplate={state.selectedQuestion.pythonTemplate} + inputTestCases={state.selectedQuestion.inputs} + outputTestCases={state.selectedQuestion.outputs} + showCodeTemplate={false} + showTestCases={true} /> ); diff --git a/frontend/src/pages/QuestionEdit/index.tsx b/frontend/src/pages/QuestionEdit/index.tsx index c3c9aa46cf..5f04cbfd91 100644 --- a/frontend/src/pages/QuestionEdit/index.tsx +++ b/frontend/src/pages/QuestionEdit/index.tsx @@ -30,6 +30,7 @@ import QuestionCategoryAutoComplete from "../../components/QuestionCategoryAutoC import QuestionDetail from "../../components/QuestionDetail"; import QuestionTestCasesFileUpload from "../../components/QuestionTestCasesFileUpload"; import QuestionCodeTemplates from "../../components/QuestionCodeTemplates"; +import { convertFileToTestCaseFormat } from "../NewQuestion"; const QuestionEdit = () => { const navigate = useNavigate(); @@ -58,6 +59,9 @@ const QuestionEdit = () => { const [uploadedImagesUrl, setUploadedImagesUrl] = useState([]); const [isPreviewQuestion, setIsPreviewQuestion] = useState(false); + const [inputTestCases, setInputTestCases] = useState([]); + const [outputTestCases, setOutputTestCases] = useState([]); + useEffect(() => { if (!questionId) { return; @@ -77,9 +81,26 @@ const QuestionEdit = () => { java: state.selectedQuestion.javaTemplate, c: state.selectedQuestion.cTemplate, }); + setInputTestCases(state.selectedQuestion.inputs); + setOutputTestCases(state.selectedQuestion.outputs); } }, [state.selectedQuestion]); + useEffect(() => { + const loadTestCases = async () => { + if (testcaseInputFile) { + setInputTestCases(await convertFileToTestCaseFormat(testcaseInputFile)); + } + if (testcaseOutputFile) { + setOutputTestCases( + await convertFileToTestCaseFormat(testcaseOutputFile) + ); + } + }; + + loadTestCases(); + }, [testcaseInputFile, testcaseOutputFile]); + const handleBack = () => { if (!confirm(ABORT_CREATE_OR_EDIT_QUESTION_CONFIRMATION_MESSAGE)) { return; @@ -125,8 +146,6 @@ const QuestionEdit = () => { description: markdownText, complexity: selectedComplexity, categories: selectedCategories, - testcaseInputFileUrl: state.selectedQuestion.testcaseInputFileUrl, - testcaseOutputFileUrl: state.selectedQuestion.testcaseOutputFileUrl, pythonTemplate: codeTemplates.python, javaTemplate: codeTemplates.java, cTemplate: codeTemplates.c, @@ -158,6 +177,13 @@ const QuestionEdit = () => { complexity={selectedComplexity} categories={selectedCategories} description={markdownText} + cTemplate={codeTemplates.c} + javaTemplate={codeTemplates.java} + pythonTemplate={codeTemplates.python} + inputTestCases={inputTestCases} + outputTestCases={outputTestCases} + showCodeTemplate={true} + showTestCases={true} /> ) : ( <> @@ -208,6 +234,7 @@ const QuestionEdit = () => { )} diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts index 7d3009310a..ada634bffd 100644 --- a/frontend/src/reducers/questionReducer.ts +++ b/frontend/src/reducers/questionReducer.ts @@ -19,18 +19,13 @@ type QuestionDetail = { description: string; complexity: string; categories: Array; - inputs: Array; - outputs: Array; pythonTemplate: string; javaTemplate: string; cTemplate: string; + inputs: string[]; + outputs: string[]; }; -// type QuestionDetailWithUrl = QuestionDetail & { -// testcaseInputFileUrl: string; -// testcaseOutputFileUrl: string; -// }; - type QuestionListDetail = { id: string; title: string; @@ -141,7 +136,7 @@ export const uploadTestcaseFiles = async ( }; export const createQuestion = async ( - question: Omit, + question: Omit, testcaseFiles: TestcaseFiles, dispatch: Dispatch ): Promise => { @@ -270,7 +265,7 @@ export const getQuestionById = ( export const updateQuestionById = async ( questionId: string, - question: Omit, + question: Omit, testcaseFiles: TestcaseFiles, dispatch: Dispatch ): Promise => { From f16b0d0a20ae39a38fa5626df2791df0d20e25f7 Mon Sep 17 00:00:00 2001 From: feliciagan <85786249+feliciagan@users.noreply.github.com> Date: Sat, 9 Nov 2024 00:29:48 +0800 Subject: [PATCH 153/192] Remove unused function --- backend/user-service/src/utils/utils.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/backend/user-service/src/utils/utils.ts b/backend/user-service/src/utils/utils.ts index f7bc06381c..447bf227b5 100644 --- a/backend/user-service/src/utils/utils.ts +++ b/backend/user-service/src/utils/utils.ts @@ -40,21 +40,3 @@ export const createFirebaseUserWithEmailAndPassword = async ( ): Promise => { return auth.createUser({ uid, email, password }); }; - -/*export const deleteFileFromFirebase = async ( - fileUrl: string -): Promise => { - return new Promise((resolve, reject) => { - const fileName = fileUrl.split('/o/')[1].split('?')[0].replace(/%2F/g, '/'); - const ref = bucket.file(fileName); - - async () => { - try { - await ref.delete(); - resolve("File deleted"); - } catch (error) { - reject(error); - } - } - }) -};*/ From e7daae43185043e31e8bbd3aa3bc33bb5479c68e Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sat, 9 Nov 2024 01:37:37 +0800 Subject: [PATCH 154/192] Fix update qn history bug --- .../src/handlers/websocketHandler.ts | 3 +- .../CollabSessionControls/index.tsx | 24 ++++-- frontend/src/contexts/CollabContext.tsx | 80 ++++++++++++------- frontend/src/utils/constants.ts | 2 + 4 files changed, 72 insertions(+), 37 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index d47f81046b..5cf333812f 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -17,6 +17,7 @@ enum CollabEvents { // Send ROOM_READY = "room_ready", + DOCUMENT_READY = "document_ready", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", END_SESSION = "end_session", @@ -31,7 +32,7 @@ const collabSessions = new Map(); const partnerReadiness = new Map(); export const handleWebsocketCollabEvents = (socket: Socket) => { - socket.on(CollabEvents.JOIN, async (uid: string, roomId: string) => { + socket.on(CollabEvents.JOIN, (uid: string, roomId: string) => { const connectionKey = `${uid}:${roomId}`; if (userConnections.has(connectionKey)) { clearTimeout(userConnections.get(connectionKey)!); diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 4fcf0b0671..680d9695af 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -7,7 +7,7 @@ import { USE_COLLAB_ERROR_MESSAGE, USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; -import { useEffect, useReducer, useState } from "react"; +import { useEffect, useReducer, useRef, useState } from "react"; import CustomDialog from "../CustomDialog"; import { extractMinutesFromTime, @@ -42,10 +42,12 @@ const CollabSessionControls: React.FC = () => { handleRejectEndSession, handleExitSession, isExitSessionModalOpen, + qnHistoryId, } = collab; const [time, setTime] = useState(0); - const [stopTime, setStopTime] = useState(false); + const [stopTime, setStopTime] = useState(true); + const timeRef = useRef(time); const [state, dispatch] = useReducer(reducer, initialState); const { selectedQuestion } = state; @@ -55,7 +57,7 @@ const CollabSessionControls: React.FC = () => { collabSocket.off(CollabEvents.PARTNER_DISCONNECTED); toast.info(COLLAB_ENDED_MESSAGE); handleConfirmEndSession( - time, + timeRef.current, setTime, setStopTime, true, @@ -66,7 +68,7 @@ const CollabSessionControls: React.FC = () => { collabSocket.once(CollabEvents.PARTNER_DISCONNECTED, () => { collabSocket.off(CollabEvents.END_SESSION); toast.error(COLLAB_PARTNER_DISCONNECTED_MESSAGE); - handleConfirmEndSession(time, setTime, setStopTime, true); + handleConfirmEndSession(timeRef.current, setTime, setStopTime, true); }); return () => { @@ -76,6 +78,8 @@ const CollabSessionControls: React.FC = () => { }, []); useEffect(() => { + timeRef.current = time; + if (stopTime) { return; } @@ -88,6 +92,12 @@ const CollabSessionControls: React.FC = () => { return () => clearInterval(intervalId); }, [time, stopTime]); + useEffect(() => { + if (qnHistoryId) { + setStopTime(false); + } + }, [qnHistoryId]); + useEffect(() => { if (!questionId) { return; @@ -105,7 +115,8 @@ const CollabSessionControls: React.FC = () => { }} variant="outlined" color="success" - onClick={() => handleSubmitSessionClick(time)} + onClick={() => handleSubmitSessionClick(timeRef.current)} + disabled={stopTime} > Submit @@ -119,6 +130,7 @@ const CollabSessionControls: React.FC = () => { onClick={() => { handleEndSessionClick(); }} + disabled={stopTime} > End Session @@ -133,7 +145,7 @@ const CollabSessionControls: React.FC = () => { } primaryAction="Confirm" handlePrimaryAction={() => - handleConfirmEndSession(time, setTime, setStopTime, false) + handleConfirmEndSession(timeRef.current, setTime, setStopTime, false) } secondaryAction="Cancel" open={isEndSessionModalOpen} diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index b9585b6462..83ff9197fc 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -1,17 +1,24 @@ /* eslint-disable react-refresh/only-export-components */ -import React, { createContext, useContext, useState } from "react"; +import React, { + createContext, + useContext, + useEffect, + useRef, + useState, +} from "react"; import { USE_MATCH_ERROR_MESSAGE, FAILED_TESTCASE_MESSAGE, SUCCESS_TESTCASE_MESSAGE, FAILED_TO_SUBMIT_CODE_MESSAGE, COLLAB_END_ERROR, + COLLAB_SUBMIT_ERROR, } from "../utils/constants"; import { toast } from "react-toastify"; import { useMatch } from "./MatchContext"; -import { codeExecutionClient } from "../utils/api"; +import { codeExecutionClient, qnHistoryClient } from "../utils/api"; import { useReducer } from "react"; import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; @@ -54,6 +61,7 @@ type CollabContextType = { checkDocReady: () => void; handleExitSession: () => void; isExitSessionModalOpen: boolean; + qnHistoryId: string | null; }; const CollabContext = createContext(null); @@ -82,7 +90,17 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [isExitSessionModalOpen, setIsExitSessionModalOpen] = useState(false); const [qnHistoryId, setQnHistoryId] = useState(null); - const [hasSubmitted, setHasSubmitted] = useState(false); + + const codeRef = useRef(code); + const qnHistoryIdRef = useRef(qnHistoryId); + + useEffect(() => { + codeRef.current = code; + }, [code]); + + useEffect(() => { + qnHistoryIdRef.current = qnHistoryId; + }, [qnHistoryId]); const handleSubmitSessionClick = async (time: number) => { try { @@ -92,8 +110,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { code: code.replace(/\t/g, " ".repeat(4)), language: matchCriteria?.language.toLowerCase(), }); - setHasSubmitted(true); - console.log([...res.data.data]); setCompilerResult([...res.data.data]); let isMatch = true; @@ -110,13 +126,18 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { toast.error(FAILED_TESTCASE_MESSAGE); } + if (!qnHistoryIdRef.current) { + toast.error(COLLAB_SUBMIT_ERROR); + return; + } + updateQnHistoryById( - qnHistoryId as string, + qnHistoryIdRef.current, { submissionStatus: isMatch ? "Accepted" : "Rejected", dateAttempted: new Date().toISOString(), timeTaken: time, - code, + code: codeRef.current, }, qnHistoryDispatch ); @@ -143,7 +164,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setIsEndSessionModalOpen(false); const roomId = getMatchId(); - if (!matchUser || !roomId || !qnHistoryId) { + if (!matchUser || !roomId || !qnHistoryIdRef.current) { toast.error(COLLAB_END_ERROR); return; } @@ -155,20 +176,26 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setTime(sessionDuration); } else { // Get question history - const data = await qnHistoryClient.get(qnHistoryId); + const data = await qnHistoryClient.get(qnHistoryIdRef.current); + + // Only update question history if it has not been submitted before + if (data.data.qnHistory.timeTaken === 0) { + updateQnHistoryById( + qnHistoryIdRef.current, + { + submissionStatus: "Attempted", + dateAttempted: new Date().toISOString(), + timeTaken: time, + code: codeRef.current.replace(/\t/g, " ".repeat(4)), + }, + qnHistoryDispatch + ); + } + } - // Only update question history if it has not been submitted before - if (!hasSubmitted) { - updateQnHistoryById( - qnHistoryId as string, - { - submissionStatus: "Attempted", - dateAttempted: new Date().toISOString(), - timeTaken: time, - code: code.replace(/\t/g, " ".repeat(4)), - }, - qnHistoryDispatch - ); + if (!isInitiatedByPartner) { + // Notify partner + collabSocket.emit(CollabEvents.END_SESSION_REQUEST, roomId, time); } // Leave collaboration room @@ -195,17 +222,9 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); }; - const checkPartnerStatus = () => { - collabSocket.on(CollabEvents.PARTNER_LEFT, () => { - toast.error(COLLAB_ENDED_MESSAGE); - setIsEndSessionModalOpen(false); - stopMatch(); - appNavigate("/home"); - }); - }; - const resetCollab = () => { setCompilerResult([]); + setQnHistoryId(null); }; return ( @@ -223,6 +242,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { checkDocReady, handleExitSession, isExitSessionModalOpen, + qnHistoryId, }} > {children} diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 87370bb819..4fd41b3d31 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -111,6 +111,8 @@ export const COLLAB_CONNECTION_ERROR = "Error connecting you to the collaboration session! Please try again."; export const COLLAB_END_ERROR = "Error ending the collaboration session! Please try again."; +export const COLLAB_SUBMIT_ERROR = + "Error submitting your attempt! Please try again."; // Code execution export const FAILED_TESTCASE_MESSAGE = From 683f032810ea1e399f611e70f889a7138a1f0b88 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sat, 9 Nov 2024 11:59:04 +0800 Subject: [PATCH 155/192] Get doc from redis if not found on server --- .../src/handlers/websocketHandler.ts | 54 ++++++++++++------- frontend/src/components/CodeEditor/index.tsx | 39 ++++++++++---- frontend/src/contexts/CollabContext.tsx | 33 +++++++++++- frontend/src/utils/collabSocket.ts | 7 ++- frontend/src/utils/constants.ts | 4 ++ 5 files changed, 102 insertions(+), 35 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 5cf333812f..26c764a7d1 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -18,6 +18,7 @@ enum CollabEvents { // Send ROOM_READY = "room_ready", DOCUMENT_READY = "document_ready", + DOCUMENT_NOT_FOUND = "document_not_found", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", END_SESSION = "end_session", @@ -109,7 +110,8 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { if (doc) { applyUpdateV2(doc, new Uint8Array(update)); } else { - // TODO: error handling + io.to(roomId).emit(CollabEvents.DOCUMENT_NOT_FOUND); + io.sockets.adapter.rooms.delete(roomId); } } ); @@ -153,24 +155,18 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { } ); - socket.on(CollabEvents.RECONNECT_REQUEST, async (roomId: string) => { - // TODO: Handle recconnection - socket.join(roomId); - - const doc = getDocument(roomId); - const storeData = await redisClient.get(`collaboration:${roomId}`); - - if (storeData) { - const tempDoc = new Doc(); - const update = Buffer.from(storeData, "base64"); - applyUpdateV2(tempDoc, new Uint8Array(update)); - const tempText = tempDoc.getText().toString(); + socket.on(CollabEvents.RECONNECT_REQUEST, (roomId: string) => { + const room = io.sockets.adapter.rooms.get(roomId); + if (!room || room.size < 2) { + socket.join(roomId); + socket.data.roomId = roomId; + } - const text = doc.getText(); - doc.transact(() => { - text.delete(0, text.length); - text.insert(0, tempText); - }); + if ( + io.sockets.adapter.rooms.get(roomId)?.size === 2 && + !collabSessions.has(roomId) + ) { + restoreDocument(roomId); } }); }; @@ -201,14 +197,32 @@ const getDocument = (roomId: string) => { return doc; }; -const saveDocument = async (roomId: string, doc: Doc) => { +const saveDocument = (roomId: string, doc: Doc) => { const docState = encodeStateAsUpdateV2(doc); const docAsString = Buffer.from(docState).toString("base64"); - await redisClient.set(`collaboration:${roomId}`, docAsString, { + redisClient.set(`collaboration:${roomId}`, docAsString, { EX: EXPIRY_TIME, }); }; +const restoreDocument = async (roomId: string) => { + const doc = getDocument(roomId); + const storeData = await redisClient.get(`collaboration:${roomId}`); + + if (storeData) { + const tempDoc = new Doc(); + const update = Buffer.from(storeData, "base64"); + applyUpdateV2(tempDoc, new Uint8Array(update)); + const tempText = tempDoc.getText().toString(); + + const text = doc.getText(); + doc.transact(() => { + text.delete(0, text.length); + text.insert(0, tempText); + }); + } +}; + const handleUserLeave = (uid: string, roomId: string, socket: Socket) => { const connectionKey = `${uid}:${roomId}`; userConnections.delete(connectionKey); diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 3fb1b3678f..f8419e6886 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -8,14 +8,17 @@ import { useEffect, useState } from "react"; import { initDocument } from "../../utils/collabSocket"; import { cursorExtension } from "../../utils/collabCursor"; import { yCollab } from "y-codemirror.next"; -import { Text } from "yjs"; +import { Doc, Text } from "yjs"; import { Awareness } from "y-protocols/awareness"; import { useCollab } from "../../contexts/CollabContext"; -import { USE_COLLAB_ERROR_MESSAGE, USE_MATCH_ERROR_MESSAGE } from "../../utils/constants"; +import { + USE_COLLAB_ERROR_MESSAGE, + USE_MATCH_ERROR_MESSAGE, +} from "../../utils/constants"; import { useMatch } from "../../contexts/MatchContext"; interface CodeEditorProps { - editorState?: { text: Text; awareness: Awareness }; + editorState?: { doc: Doc; text: Text; awareness: Awareness }; uid?: string; username?: string; language: string; @@ -46,7 +49,8 @@ const CodeEditor: React.FC = (props) => { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { matchCriteria, matchUser, partner, questionId, questionTitle } = match; + const { matchCriteria, matchUser, partner, questionId, questionTitle } = + match; const collab = useCollab(); if (!collab) { @@ -69,14 +73,29 @@ const CodeEditor: React.FC = (props) => { }; useEffect(() => { - if (isReadOnly || !isEditorReady) { + if (isReadOnly || !isEditorReady || !editorState) { return; } const loadTemplate = async () => { - if (matchUser && partner && matchCriteria && questionId && questionTitle) { - checkDocReady(); - await initDocument(uid, roomId, template, matchUser.id, partner.id, matchCriteria.language, questionId, questionTitle); + if ( + matchUser && + partner && + matchCriteria && + questionId && + questionTitle + ) { + checkDocReady(roomId, editorState.doc, setIsDocumentLoaded); + await initDocument( + uid, + roomId, + template, + matchUser.id, + partner.id, + matchCriteria.language, + questionId, + questionTitle + ); setIsDocumentLoaded(true); } }; @@ -109,9 +128,7 @@ const CodeEditor: React.FC = (props) => { ]} value={isReadOnly ? template : undefined} placeholder={ - !isReadOnly && !isDocumentLoaded - ? "Loading code template..." - : undefined + !isReadOnly && !isDocumentLoaded ? "Loading the code..." : undefined } /> ); diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 83ff9197fc..e631c738d4 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -14,6 +14,8 @@ import { FAILED_TO_SUBMIT_CODE_MESSAGE, COLLAB_END_ERROR, COLLAB_SUBMIT_ERROR, + COLLAB_DOCUMENT_ERROR, + COLLAB_DOCUMENT_RESTORED, } from "../utils/constants"; import { toast } from "react-toastify"; @@ -28,6 +30,7 @@ import { communicationSocket, } from "../utils/communicationSocket"; import useAppNavigate from "../hooks/useAppNavigate"; +import { applyUpdateV2, Doc } from "yjs"; export type CompilerResult = { status: string; @@ -58,7 +61,11 @@ type CollabContextType = { setCompilerResult: React.Dispatch>; isEndSessionModalOpen: boolean; resetCollab: () => void; - checkDocReady: () => void; + checkDocReady: ( + roomId: string, + doc: Doc, + setIsDocumentLoaded: React.Dispatch> + ) => void; handleExitSession: () => void; isExitSessionModalOpen: boolean; qnHistoryId: string | null; @@ -216,10 +223,32 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { resetCollab(); }; - const checkDocReady = () => { + const checkDocReady = ( + roomId: string, + doc: Doc, + setIsDocumentLoaded: React.Dispatch> + ) => { collabSocket.on(CollabEvents.DOCUMENT_READY, (qnHistoryId: string) => { setQnHistoryId(qnHistoryId); }); + + collabSocket.on(CollabEvents.DOCUMENT_NOT_FOUND, () => { + toast.error(COLLAB_DOCUMENT_ERROR); + setIsDocumentLoaded(false); + + const text = doc.getText(); + doc.transact(() => { + text.delete(0, text.length); + }, matchUser?.id); + + collabSocket.once(CollabEvents.UPDATE, (update) => { + applyUpdateV2(doc, new Uint8Array(update), matchUser?.id); + toast.success(COLLAB_DOCUMENT_RESTORED); + setIsDocumentLoaded(true); + }); + + collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); + }); }; const resetCollab = () => { diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 82e4afe295..f5d997ee15 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -18,10 +18,12 @@ export enum CollabEvents { // Receive ROOM_READY = "room_ready", DOCUMENT_READY = "document_ready", + DOCUMENT_NOT_FOUND = "document_not_found", UPDATE = "updateV2", UPDATE_CURSOR = "update_cursor", END_SESSION = "end_session", PARTNER_DISCONNECTED = "partner_disconnected", + SOCKET_DISCONNECT = "disconnect", SOCKET_CLIENT_DISCONNECT = "io client disconnect", SOCKET_SERVER_DISCONNECT = "io server disconnect", @@ -31,6 +33,7 @@ export enum CollabEvents { export type CollabSessionData = { ready: boolean; + doc: Doc; text: Text; awareness: Awareness; }; @@ -61,7 +64,7 @@ export const join = ( awareness = new Awareness(doc); doc.on(CollabEvents.UPDATE, (update, origin) => { - if (origin != uid) { + if (origin !== uid) { collabSocket.emit(CollabEvents.UPDATE_REQUEST, roomId, update); } }); @@ -74,7 +77,7 @@ export const join = ( return new Promise((resolve) => { collabSocket.once(CollabEvents.ROOM_READY, (ready: boolean) => { - resolve({ ready: ready, text: text, awareness: awareness }); + resolve({ ready: ready, doc: doc, text: text, awareness: awareness }); }); }); }; diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 4fd41b3d31..c34e63b7cd 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -113,6 +113,10 @@ export const COLLAB_END_ERROR = "Error ending the collaboration session! Please try again."; export const COLLAB_SUBMIT_ERROR = "Error submitting your attempt! Please try again."; +export const COLLAB_DOCUMENT_ERROR = + "Error syncing the code! Please wait as we try to reconnect. Recent changes may be lost."; +export const COLLAB_DOCUMENT_RESTORED = + "Connection restored! You may resume editing the code."; // Code execution export const FAILED_TESTCASE_MESSAGE = From d7aaa902128cdaeb13bbc53f87337220d8bf0f60 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sat, 9 Nov 2024 13:19:48 +0800 Subject: [PATCH 156/192] Handle collab end on client refresh --- .../src/handlers/websocketHandler.ts | 10 +- .../src/handlers/websocketHandler.ts | 35 ++++--- frontend/src/components/CodeEditor/index.tsx | 2 +- frontend/src/contexts/CollabContext.tsx | 91 ++++++++++++++----- frontend/src/contexts/MatchContext.tsx | 7 +- frontend/src/pages/CollabSandbox/index.tsx | 2 + frontend/src/utils/collabSocket.ts | 33 +------ frontend/src/utils/constants.ts | 6 +- 8 files changed, 99 insertions(+), 87 deletions(-) diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 26c764a7d1..3a049e2d18 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -37,7 +37,6 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { const connectionKey = `${uid}:${roomId}`; if (userConnections.has(connectionKey)) { clearTimeout(userConnections.get(connectionKey)!); - return; } userConnections.set(connectionKey, null); @@ -50,11 +49,10 @@ export const handleWebsocketCollabEvents = (socket: Socket) => { socket.join(roomId); socket.data.roomId = roomId; - if ( - io.sockets.adapter.rooms.get(roomId)?.size === 2 && - !collabSessions.has(roomId) - ) { - createCollabSession(roomId); + if (io.sockets.adapter.rooms.get(roomId)?.size === 2) { + if (!collabSessions.has(roomId)) { + createCollabSession(roomId); + } io.to(roomId).emit(CollabEvents.ROOM_READY, true); } }); diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index a2065f7bbc..92f9861836 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -119,27 +119,24 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { userConnections.delete(uid); }); - socket.on( - MatchEvents.MATCH_ACCEPT_REQUEST, - (matchId: string, userId1: string, userId2: string) => { - const partnerAccepted = handleMatchAccept(matchId); - if (partnerAccepted) { - const match = getMatchById(matchId); - if (!match) { - return; - } - - const { complexity, category } = match; - getRandomQuestion(complexity, category).then((res) => { - io.to(matchId).emit( - MatchEvents.MATCH_SUCCESSFUL, - res.data.question.id, - res.data.question.title - ); - }); + socket.on(MatchEvents.MATCH_ACCEPT_REQUEST, (matchId: string) => { + const partnerAccepted = handleMatchAccept(matchId); + if (partnerAccepted) { + const match = getMatchById(matchId); + if (!match) { + return; } + + const { complexity, category } = match; + getRandomQuestion(complexity, category).then((res) => { + io.to(matchId).emit( + MatchEvents.MATCH_SUCCESSFUL, + res.data.question.id, + res.data.question.title + ); + }); } - ); + }); socket.on( MatchEvents.MATCH_DECLINE_REQUEST, diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index f8419e6886..b21c4f142b 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -101,7 +101,7 @@ const CodeEditor: React.FC = (props) => { }; loadTemplate(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isReadOnly, isEditorReady]); + }, [isReadOnly, isEditorReady, editorState]); return ( = (props) => { const roomId = getMatchId(); if (!matchUser || !roomId || !qnHistoryIdRef.current) { toast.error(COLLAB_END_ERROR); + appNavigate("/home"); return; } @@ -213,8 +215,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const handleExitSession = () => { - setIsExitSessionModalOpen(false); - // Delete match data stopMatch(); appNavigate("/home"); @@ -228,31 +228,80 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { doc: Doc, setIsDocumentLoaded: React.Dispatch> ) => { - collabSocket.on(CollabEvents.DOCUMENT_READY, (qnHistoryId: string) => { - setQnHistoryId(qnHistoryId); - }); - - collabSocket.on(CollabEvents.DOCUMENT_NOT_FOUND, () => { - toast.error(COLLAB_DOCUMENT_ERROR); - setIsDocumentLoaded(false); - - const text = doc.getText(); - doc.transact(() => { - text.delete(0, text.length); - }, matchUser?.id); - - collabSocket.once(CollabEvents.UPDATE, (update) => { - applyUpdateV2(doc, new Uint8Array(update), matchUser?.id); - toast.success(COLLAB_DOCUMENT_RESTORED); - setIsDocumentLoaded(true); + if (!collabSocket.hasListeners(CollabEvents.DOCUMENT_READY)) { + collabSocket.on(CollabEvents.DOCUMENT_READY, (qnHistoryId: string) => { + setQnHistoryId(qnHistoryId); }); + } + + if (!collabSocket.hasListeners(CollabEvents.DOCUMENT_NOT_FOUND)) { + collabSocket.on(CollabEvents.DOCUMENT_NOT_FOUND, () => { + toast.error(COLLAB_DOCUMENT_ERROR); + setIsDocumentLoaded(false); + + const text = doc.getText(); + doc.transact(() => { + text.delete(0, text.length); + }, matchUser?.id); + + collabSocket.once(CollabEvents.UPDATE, (update) => { + applyUpdateV2(doc, new Uint8Array(update), matchUser?.id); + toast.success(COLLAB_DOCUMENT_RESTORED); + setIsDocumentLoaded(true); + }); + + collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); + }); + } + + if (!collabSocket.hasListeners(CollabEvents.SOCKET_DISCONNECT)) { + collabSocket.on(CollabEvents.SOCKET_DISCONNECT, (reason) => { + console.log(reason); + if ( + reason !== CollabEvents.SOCKET_CLIENT_DISCONNECT && + reason !== CollabEvents.SOCKET_SERVER_DISCONNECT + ) { + toast.error(COLLAB_DOCUMENT_ERROR); + setIsDocumentLoaded(false); + } + }); + } - collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); - }); + if (!collabSocket.io.hasListeners(CollabEvents.SOCKET_RECONNECT_SUCCESS)) { + collabSocket.io.on(CollabEvents.SOCKET_RECONNECT_SUCCESS, () => { + const text = doc.getText(); + doc.transact(() => { + text.delete(0, text.length); + }, matchUser?.id); + + collabSocket.once(CollabEvents.UPDATE, (update) => { + applyUpdateV2(doc, new Uint8Array(update), matchUser?.id); + toast.success(COLLAB_DOCUMENT_RESTORED); + setIsDocumentLoaded(true); + }); + + collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); + }); + } + + if (!collabSocket.io.hasListeners(CollabEvents.SOCKET_RECONNECT_FAILED)) { + collabSocket.io.on(CollabEvents.SOCKET_RECONNECT_FAILED, () => { + toast.error(COLLAB_RECONNECTION_ERROR); + + if (matchUser) { + leave(matchUser.id, roomId, true); + } + communicationSocket.emit(CommunicationEvents.USER_DISCONNECT); + + handleExitSession(); + }); + } }; const resetCollab = () => { setCompilerResult([]); + setIsEndSessionModalOpen(false); + setIsExitSessionModalOpen(false); setQnHistoryId(null); }; diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index e2651d5f5d..32b0e2dfb9 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -391,12 +391,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { return; } - matchSocket.emit( - MatchEvents.MATCH_ACCEPT_REQUEST, - matchId, - matchUser.id, - partner.id - ); + matchSocket.emit(MatchEvents.MATCH_ACCEPT_REQUEST, matchId); }; const rematch = () => { diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 7fa0fbcb2a..6deb92e314 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -52,6 +52,8 @@ const CollabSandbox: React.FC = () => { resetCollab(); if (!matchUser || !matchId) { + toast.error(COLLAB_CONNECTION_ERROR); + setIsConnecting(false); return; } diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index f5d997ee15..c0f2d72bf9 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -41,7 +41,7 @@ export type CollabSessionData = { const COLLAB_SOCKET_URL = "http://localhost:3003"; export const collabSocket = io(COLLAB_SOCKET_URL, { - reconnectionAttempts: 3, + reconnectionAttempts: 5, autoConnect: false, auth: { token: getToken(), @@ -57,7 +57,6 @@ export const join = ( roomId: string ): Promise => { collabSocket.connect(); - initConnectionStatusListeners(roomId); doc = new Doc(); text = doc.getText(); @@ -141,33 +140,3 @@ export const receiveCursorUpdate = (view: EditorView) => { }); }); }; - -export const reconnectRequest = (roomId: string) => { - collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); -}; - -const initConnectionStatusListeners = (roomId: string) => { - if (!collabSocket.hasListeners(CollabEvents.SOCKET_DISCONNECT)) { - collabSocket.on(CollabEvents.SOCKET_DISCONNECT, (reason) => { - if ( - reason !== CollabEvents.SOCKET_CLIENT_DISCONNECT && - reason !== CollabEvents.SOCKET_SERVER_DISCONNECT - ) { - // TODO: Handle socket disconnection - } - }); - } - - if (!collabSocket.io.hasListeners(CollabEvents.SOCKET_RECONNECT_SUCCESS)) { - collabSocket.io.on(CollabEvents.SOCKET_RECONNECT_SUCCESS, () => { - console.log("reconnect request"); - collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); - }); - } - - if (!collabSocket.io.hasListeners(CollabEvents.SOCKET_RECONNECT_FAILED)) { - collabSocket.io.on(CollabEvents.SOCKET_RECONNECT_FAILED, () => { - console.log("reconnect failed"); - }); - } -}; diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index c34e63b7cd..0cdb7441c0 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -108,9 +108,11 @@ export const COLLAB_ENDED_MESSAGE = export const COLLAB_PARTNER_DISCONNECTED_MESSAGE = "Unfortunately, the collaboration session has ended as your partner has disconnected."; export const COLLAB_CONNECTION_ERROR = - "Error connecting you to the collaboration session! Please try again."; + "Error connecting you to the collaboration session! Please find another match."; +export const COLLAB_RECONNECTION_ERROR = + "Error reconnecting you to the collaboration session! Closing the session..."; export const COLLAB_END_ERROR = - "Error ending the collaboration session! Please try again."; + "Something went wrong! Forcefully ending the session..."; export const COLLAB_SUBMIT_ERROR = "Error submitting your attempt! Please try again."; export const COLLAB_DOCUMENT_ERROR = From 77e231aa85255a4c7ce1755b215cd128d4d00bfd Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Sat, 9 Nov 2024 14:21:41 +0800 Subject: [PATCH 157/192] Prevent code editor from re-rendering on code change --- frontend/src/components/CodeEditor/index.tsx | 7 +--- .../CollabSessionControls/index.tsx | 17 +++------ frontend/src/contexts/CollabContext.tsx | 37 +++++++++++-------- frontend/src/utils/collabSocket.ts | 6 +++ 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index b21c4f142b..f74b69aa80 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -57,7 +57,7 @@ const CodeEditor: React.FC = (props) => { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { setCode, checkDocReady } = collab; + const { checkDocReady } = collab; const [isEditorReady, setIsEditorReady] = useState(false); const [isDocumentLoaded, setIsDocumentLoaded] = useState(false); @@ -68,10 +68,6 @@ const CodeEditor: React.FC = (props) => { } }; - const handleChange = (value: string) => { - setCode(value); - }; - useEffect(() => { if (isReadOnly || !isEditorReady || !editorState) { return; @@ -111,7 +107,6 @@ const CodeEditor: React.FC = (props) => { width="100%" basicSetup={false} id="codeEditor" - onChange={handleChange} extensions={[ indentUnit.of("\t"), basicSetup(), diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 680d9695af..4c0547e9cf 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -43,10 +43,11 @@ const CollabSessionControls: React.FC = () => { handleExitSession, isExitSessionModalOpen, qnHistoryId, + stopTime, + setStopTime, } = collab; const [time, setTime] = useState(0); - const [stopTime, setStopTime] = useState(true); const timeRef = useRef(time); const [state, dispatch] = useReducer(reducer, initialState); @@ -56,19 +57,13 @@ const CollabSessionControls: React.FC = () => { collabSocket.once(CollabEvents.END_SESSION, (sessionDuration: number) => { collabSocket.off(CollabEvents.PARTNER_DISCONNECTED); toast.info(COLLAB_ENDED_MESSAGE); - handleConfirmEndSession( - timeRef.current, - setTime, - setStopTime, - true, - sessionDuration - ); + handleConfirmEndSession(timeRef.current, setTime, true, sessionDuration); }); collabSocket.once(CollabEvents.PARTNER_DISCONNECTED, () => { collabSocket.off(CollabEvents.END_SESSION); toast.error(COLLAB_PARTNER_DISCONNECTED_MESSAGE); - handleConfirmEndSession(timeRef.current, setTime, setStopTime, true); + handleConfirmEndSession(timeRef.current, setTime, true); }); return () => { @@ -115,7 +110,7 @@ const CollabSessionControls: React.FC = () => { }} variant="outlined" color="success" - onClick={() => handleSubmitSessionClick(timeRef.current)} + onClick={() => handleSubmitSessionClick(time)} disabled={stopTime} > Submit @@ -145,7 +140,7 @@ const CollabSessionControls: React.FC = () => { } primaryAction="Confirm" handlePrimaryAction={() => - handleConfirmEndSession(timeRef.current, setTime, setStopTime, false) + handleConfirmEndSession(time, setTime, false) } secondaryAction="Cancel" open={isEndSessionModalOpen} diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index eae4b600d5..3167b81b4d 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -25,7 +25,12 @@ import { codeExecutionClient, qnHistoryClient } from "../utils/api"; import { useReducer } from "react"; import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; -import { CollabEvents, collabSocket, leave } from "../utils/collabSocket"; +import { + CollabEvents, + collabSocket, + getDocContent, + leave, +} from "../utils/collabSocket"; import { CommunicationEvents, communicationSocket, @@ -53,11 +58,9 @@ type CollabContextType = { handleConfirmEndSession: ( time: number, setTime: React.Dispatch>, - setStopTime: React.Dispatch>, isInitiatedByPartner: boolean, sessionDuration?: number ) => void; - setCode: React.Dispatch>; compilerResult: CompilerResult[]; setCompilerResult: React.Dispatch>; isEndSessionModalOpen: boolean; @@ -70,6 +73,8 @@ type CollabContextType = { handleExitSession: () => void; isExitSessionModalOpen: boolean; qnHistoryId: string | null; + stopTime: boolean; + setStopTime: React.Dispatch>; }; const CollabContext = createContext(null); @@ -91,31 +96,27 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { qnHistoryReducer, initialQHState ); - const [code, setCode] = useState(""); + const [compilerResult, setCompilerResult] = useState([]); const [isEndSessionModalOpen, setIsEndSessionModalOpen] = useState(false); const [isExitSessionModalOpen, setIsExitSessionModalOpen] = useState(false); const [qnHistoryId, setQnHistoryId] = useState(null); + const [stopTime, setStopTime] = useState(true); - const codeRef = useRef(code); const qnHistoryIdRef = useRef(qnHistoryId); - useEffect(() => { - codeRef.current = code; - }, [code]); - useEffect(() => { qnHistoryIdRef.current = qnHistoryId; }, [qnHistoryId]); const handleSubmitSessionClick = async (time: number) => { + const code = getDocContent(); try { const res = await codeExecutionClient.post("/", { questionId, - // Replace tabs with 4 spaces to prevent formatting issues - code: code.replace(/\t/g, " ".repeat(4)), + code: code, language: matchCriteria?.language.toLowerCase(), }); setCompilerResult([...res.data.data]); @@ -145,7 +146,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { submissionStatus: isMatch ? "Accepted" : "Rejected", dateAttempted: new Date().toISOString(), timeTaken: time, - code: codeRef.current, + code: code, }, qnHistoryDispatch ); @@ -165,7 +166,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const handleConfirmEndSession = async ( time: number, setTime: React.Dispatch>, - setStopTime: React.Dispatch>, isInitiatedByPartner: boolean, sessionDuration?: number ) => { @@ -189,13 +189,15 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { // Only update question history if it has not been submitted before if (data.data.qnHistory.timeTaken === 0) { + const code = getDocContent(); + updateQnHistoryById( qnHistoryIdRef.current, { submissionStatus: "Attempted", dateAttempted: new Date().toISOString(), timeTaken: time, - code: codeRef.current.replace(/\t/g, " ".repeat(4)), + code: code, }, qnHistoryDispatch ); @@ -238,6 +240,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { collabSocket.on(CollabEvents.DOCUMENT_NOT_FOUND, () => { toast.error(COLLAB_DOCUMENT_ERROR); setIsDocumentLoaded(false); + setStopTime(true); const text = doc.getText(); doc.transact(() => { @@ -248,6 +251,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { applyUpdateV2(doc, new Uint8Array(update), matchUser?.id); toast.success(COLLAB_DOCUMENT_RESTORED); setIsDocumentLoaded(true); + setStopTime(false); }); collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); @@ -263,6 +267,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { ) { toast.error(COLLAB_DOCUMENT_ERROR); setIsDocumentLoaded(false); + setStopTime(true); } }); } @@ -278,6 +283,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { applyUpdateV2(doc, new Uint8Array(update), matchUser?.id); toast.success(COLLAB_DOCUMENT_RESTORED); setIsDocumentLoaded(true); + setStopTime(false); }); collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); @@ -312,7 +318,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { handleEndSessionClick, handleRejectEndSession, handleConfirmEndSession, - setCode, compilerResult, setCompilerResult, isEndSessionModalOpen, @@ -321,6 +326,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { handleExitSession, isExitSessionModalOpen, qnHistoryId, + stopTime, + setStopTime, }} > {children} diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index c0f2d72bf9..9da9fc56b3 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -140,3 +140,9 @@ export const receiveCursorUpdate = (view: EditorView) => { }); }); }; + +export const getDocContent = () => { + return doc && !doc.isDestroyed + ? doc.getText().toString().replace(/\t/g, " ".repeat(4)) // Replace tabs with 4 spaces to prevent formatting issues + : ""; +}; From 40700b441567b1df0e490b08425bd3c1f5283ba1 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:17:13 +0800 Subject: [PATCH 158/192] Fix test --- .../QuestionImage/QuestionImage.test.tsx | 14 ++++++-------- frontend/src/components/QuestionImage/index.tsx | 8 +++++--- frontend/src/theme.ts | 14 +++++++------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/QuestionImage/QuestionImage.test.tsx b/frontend/src/components/QuestionImage/QuestionImage.test.tsx index 8d33e517be..1b602d08f7 100644 --- a/frontend/src/components/QuestionImage/QuestionImage.test.tsx +++ b/frontend/src/components/QuestionImage/QuestionImage.test.tsx @@ -1,12 +1,6 @@ import { fireEvent, render, screen } from "@testing-library/react"; import QuestionImage from "."; -Object.assign(navigator, { - clipboard: { - writeText: jest.fn(), - }, -}); - describe("Question Image", () => { const url = "https://example.com/image.jpg"; const mockHandleClickOpen = jest.fn(); @@ -15,19 +9,23 @@ describe("Question Image", () => { render(); const image = screen.getByAltText("question image"); - expect(image).toBeInTheDocument(); }); it("Copy Question Image url", () => { + const promptSpy = jest.spyOn(window, "prompt").mockImplementation(() => ""); + render(); const copyButton = screen.getByLabelText("copy"); fireEvent.click(copyButton); - expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + expect(promptSpy).toHaveBeenCalledWith( + "Copy to clipboard: Ctrl+C, Enter", `![image](${url})` ); + + promptSpy.mockRestore(); }); it("Expand Question Image", () => { diff --git a/frontend/src/components/QuestionImage/index.tsx b/frontend/src/components/QuestionImage/index.tsx index 9314d8c616..4bd7cccd65 100644 --- a/frontend/src/components/QuestionImage/index.tsx +++ b/frontend/src/components/QuestionImage/index.tsx @@ -1,7 +1,6 @@ import { Box, ImageListItem, IconButton } from "@mui/material"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import FullscreenIcon from "@mui/icons-material/Fullscreen"; -import { toast } from "react-toastify"; interface QuestionImageProps { url: string; @@ -56,8 +55,11 @@ const QuestionImage: React.FC = ({ > { - navigator.clipboard.writeText(`![image](${url})`); - toast.success("Image URL copied to clipboard"); + // switch to window.prompt since navigator.clipboard.writeText is not supported in HTTP + window.prompt( + "Copy to clipboard: Ctrl+C, Enter", + `![image](${url})` + ); }} sx={{ color: "#fff" }} aria-label="copy" diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts index 1b7739f405..70d5f52120 100644 --- a/frontend/src/theme.ts +++ b/frontend/src/theme.ts @@ -1,4 +1,4 @@ -import grey from "@mui/material/colors/grey"; +import { grey } from "@mui/material/colors"; import { createTheme } from "@mui/material/styles"; const theme = createTheme({ @@ -47,13 +47,13 @@ const theme = createTheme({ }, }, }, - MuiListItemText: { - styleOverrides: { - primary: { - fontSize: "14px", - }, + MuiListItemText: { + styleOverrides: { + primary: { + fontSize: "14px", }, - } + }, + }, }, }); From e8cc2594f41d1e63c3047fb8c8d0da5134eca23d Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Sun, 10 Nov 2024 15:01:13 +0800 Subject: [PATCH 159/192] Fix qns history --- frontend/src/pages/QuestionHistoryDetail/index.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/pages/QuestionHistoryDetail/index.tsx b/frontend/src/pages/QuestionHistoryDetail/index.tsx index e94c9cd994..cbf01d6242 100644 --- a/frontend/src/pages/QuestionHistoryDetail/index.tsx +++ b/frontend/src/pages/QuestionHistoryDetail/index.tsx @@ -210,6 +210,13 @@ const QuestionHistoryDetail: React.FC = () => { complexity={qnState.selectedQuestion.complexity} categories={qnState.selectedQuestion.categories} description={qnState.selectedQuestion.description} + cTemplate={qnState.selectedQuestion.cTemplate} + javaTemplate={qnState.selectedQuestion.javaTemplate} + pythonTemplate={qnState.selectedQuestion.pythonTemplate} + inputTestCases={qnState.selectedQuestion.inputs} + outputTestCases={qnState.selectedQuestion.outputs} + showCodeTemplate={false} + showTestCases={true} /> ) : ( Date: Sun, 10 Nov 2024 16:39:38 +0800 Subject: [PATCH 160/192] Define location state type --- frontend/src/components/NoDirectAccessRoutes/index.tsx | 3 ++- frontend/src/hooks/useAppNavigate.tsx | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/NoDirectAccessRoutes/index.tsx b/frontend/src/components/NoDirectAccessRoutes/index.tsx index 0af10ae54c..2dea467c8d 100644 --- a/frontend/src/components/NoDirectAccessRoutes/index.tsx +++ b/frontend/src/components/NoDirectAccessRoutes/index.tsx @@ -1,9 +1,10 @@ import { Navigate, Outlet, useLocation } from "react-router-dom"; +import { LocationState } from "../../hooks/useAppNavigate"; const NoDirectAccessRoutes: React.FC = () => { const location = useLocation(); - if (location.state?.from !== "app-navigation") { + if ((location.state as LocationState)?.from !== "app-navigation") { return ; } diff --git a/frontend/src/hooks/useAppNavigate.tsx b/frontend/src/hooks/useAppNavigate.tsx index d68826bf65..873078c4e5 100644 --- a/frontend/src/hooks/useAppNavigate.tsx +++ b/frontend/src/hooks/useAppNavigate.tsx @@ -1,12 +1,16 @@ import { useNavigate } from "react-router-dom"; +export interface LocationState { + from: string; +} + export const useAppNavigate = () => { const navigate = useNavigate(); const appNavigate = (path: string) => { navigate(path, { replace: location.pathname !== "/home", - state: { from: "app-navigation" }, + state: { from: "app-navigation" } as LocationState, }); }; From 1961f3c1755b2b783a572cae33045d91e77247a6 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 10 Nov 2024 17:44:34 +0800 Subject: [PATCH 161/192] Add build script --- backend/code-execution-service/Dockerfile | 14 +- backend/code-execution-service/package.json | 5 +- .../code-execution-service/{ => src}/app.ts | 2 +- .../{ => src}/server.ts | 0 backend/code-execution-service/tsconfig.json | 14 +- backend/collab-service/Dockerfile | 14 +- backend/collab-service/package.json | 3 +- backend/collab-service/src/app.ts | 2 +- .../src/middlewares/basicAccessControl.ts | 2 +- backend/collab-service/src/server.ts | 8 +- backend/collab-service/tsconfig.json | 14 +- backend/communication-service/Dockerfile | 16 +- backend/communication-service/package.json | 3 +- backend/communication-service/tsconfig.json | 14 +- backend/matching-service/Dockerfile | 14 +- backend/matching-service/package.json | 3 +- backend/matching-service/src/app.ts | 2 +- backend/matching-service/src/server.ts | 8 +- backend/matching-service/tsconfig.json | 12 +- backend/qn-history-service/Dockerfile | 14 +- backend/qn-history-service/package.json | 3 +- backend/qn-history-service/src/app.ts | 2 +- .../controllers/questionHistoryController.ts | 6 +- backend/qn-history-service/src/server.ts | 4 +- backend/qn-history-service/tsconfig.json | 26 +- backend/question-service/Dockerfile | 14 +- backend/question-service/package.json | 3 +- backend/question-service/src/app.ts | 2 +- .../src/controllers/questionController.ts | 10 +- .../src/routes/questionRoutes.ts | 4 +- backend/question-service/src/server.ts | 4 +- backend/question-service/tsconfig.json | 14 +- backend/user-service/Dockerfile | 14 +- backend/user-service/package.json | 5 +- backend/user-service/src/server.ts | 8 +- backend/user-service/tsconfig.json | 14 +- docker-compose-prod.yml | 266 ++++++++++++++++++ docker-compose-test.yml | 40 ++- docker-compose.yml | 40 ++- frontend/Dockerfile | 16 +- .../src/components/Navbar/Navbar.test.tsx | 3 + ...etailstest.tsx => ProfileDetails.test.tsx} | 0 42 files changed, 548 insertions(+), 114 deletions(-) rename backend/code-execution-service/{ => src}/app.ts (92%) rename backend/code-execution-service/{ => src}/server.ts (100%) create mode 100644 docker-compose-prod.yml rename frontend/src/components/ProfileDetails/{ProfileDetailstest.tsx => ProfileDetails.test.tsx} (100%) diff --git a/backend/code-execution-service/Dockerfile b/backend/code-execution-service/Dockerfile index 3e6037c84b..257d6f6fbb 100644 --- a/backend/code-execution-service/Dockerfile +++ b/backend/code-execution-service/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-alpine AS base WORKDIR /code-execution-service @@ -10,4 +10,16 @@ COPY . . EXPOSE 3004 +# DEV + +FROM base AS dev + CMD ["npm", "run", "dev"] + +# PROD + +FROM base AS prod + +RUN npm run build + +CMD ["npm", "start"] diff --git a/backend/code-execution-service/package.json b/backend/code-execution-service/package.json index 984729f9e0..29661dde80 100644 --- a/backend/code-execution-service/package.json +++ b/backend/code-execution-service/package.json @@ -4,8 +4,9 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx server.ts", - "dev": "tsx watch server.ts", + "start": "tsx dist/server.js", + "dev": "tsx watch src/server.ts", + "build": "tsc", "test": "cross-env NODE_ENV=test && jest", "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." diff --git a/backend/code-execution-service/app.ts b/backend/code-execution-service/src/app.ts similarity index 92% rename from backend/code-execution-service/app.ts rename to backend/code-execution-service/src/app.ts index 59942f969e..16d941bd6b 100644 --- a/backend/code-execution-service/app.ts +++ b/backend/code-execution-service/src/app.ts @@ -5,7 +5,7 @@ import yaml from "yaml"; import swaggerUi from "swagger-ui-express"; import cors from "cors"; -import codeExecutionRoutes from "./src/routes/codeExecutionRoutes.ts"; +import codeExecutionRoutes from "./routes/codeExecutionRoutes"; dotenv.config(); diff --git a/backend/code-execution-service/server.ts b/backend/code-execution-service/src/server.ts similarity index 100% rename from backend/code-execution-service/server.ts rename to backend/code-execution-service/src/server.ts diff --git a/backend/code-execution-service/tsconfig.json b/backend/code-execution-service/tsconfig.json index 830b218c6e..ce40ce2979 100644 --- a/backend/code-execution-service/tsconfig.json +++ b/backend/code-execution-service/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "ES2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ + "rootDir": "./src" /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -35,7 +35,7 @@ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, + // "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ @@ -55,9 +55,9 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "noEmit": true /* Disable emitting files from a compilation. */, + // "noEmit": true, /* Disable emitting files from a compilation. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ @@ -106,5 +106,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/backend/collab-service/Dockerfile b/backend/collab-service/Dockerfile index 9f34638d16..b632086800 100644 --- a/backend/collab-service/Dockerfile +++ b/backend/collab-service/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-alpine AS base WORKDIR /collab-service @@ -10,4 +10,16 @@ COPY . . EXPOSE 3003 +# DEV + +FROM base AS dev + CMD ["npm", "run", "dev"] + +# PROD + +FROM base AS prod + +RUN npm run build + +CMD ["npm", "start"] diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index 5529171a82..48960907f5 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -4,8 +4,9 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx src/server.ts", + "start": "tsx src/server.js", "dev": "tsx watch src/server.ts", + "build": "tsc", "test": "cross-env NODE_ENV=test && jest", "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." diff --git a/backend/collab-service/src/app.ts b/backend/collab-service/src/app.ts index 9aaff3af65..d42956da58 100644 --- a/backend/collab-service/src/app.ts +++ b/backend/collab-service/src/app.ts @@ -5,7 +5,7 @@ import yaml from "yaml"; import swaggerUi from "swagger-ui-express"; import cors from "cors"; -import collabRoutes from "./routes/collabRoutes.ts"; +import collabRoutes from "./routes/collabRoutes"; dotenv.config(); diff --git a/backend/collab-service/src/middlewares/basicAccessControl.ts b/backend/collab-service/src/middlewares/basicAccessControl.ts index 727ee6783e..15088e9a86 100644 --- a/backend/collab-service/src/middlewares/basicAccessControl.ts +++ b/backend/collab-service/src/middlewares/basicAccessControl.ts @@ -1,5 +1,5 @@ import { ExtendedError, Socket } from "socket.io"; -import { verifyToken } from "../api/userService.ts"; +import { verifyToken } from "../api/userService"; export const verifyUserToken = ( socket: Socket, diff --git a/backend/collab-service/src/server.ts b/backend/collab-service/src/server.ts index 1a00c9c42c..0730768241 100644 --- a/backend/collab-service/src/server.ts +++ b/backend/collab-service/src/server.ts @@ -1,9 +1,9 @@ import http from "http"; -import app, { allowedOrigins } from "./app.ts"; -import { handleWebsocketCollabEvents } from "./handlers/websocketHandler.ts"; +import app, { allowedOrigins } from "./app"; +import { handleWebsocketCollabEvents } from "./handlers/websocketHandler"; import { Server, Socket } from "socket.io"; -import { connectRedis } from "./config/redis.ts"; -import { verifyUserToken } from "./middlewares/basicAccessControl.ts"; +import { connectRedis } from "./config/redis"; +import { verifyUserToken } from "./middlewares/basicAccessControl"; const server = http.createServer(app); diff --git a/backend/collab-service/tsconfig.json b/backend/collab-service/tsconfig.json index 830b218c6e..ce40ce2979 100644 --- a/backend/collab-service/tsconfig.json +++ b/backend/collab-service/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "ES2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ + "rootDir": "./src" /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -35,7 +35,7 @@ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, + // "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ @@ -55,9 +55,9 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "noEmit": true /* Disable emitting files from a compilation. */, + // "noEmit": true, /* Disable emitting files from a compilation. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ @@ -106,5 +106,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/backend/communication-service/Dockerfile b/backend/communication-service/Dockerfile index e38cd4dca4..a52d6a4e1b 100644 --- a/backend/communication-service/Dockerfile +++ b/backend/communication-service/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-alpine AS base WORKDIR /communication-service @@ -10,4 +10,16 @@ COPY . . EXPOSE 3005 -CMD ["npm", "run", "dev"] \ No newline at end of file +# DEV + +FROM base AS dev + +CMD ["npm", "run", "dev"] + +# PROD + +FROM base AS prod + +RUN npm run build + +CMD ["npm", "start"] \ No newline at end of file diff --git a/backend/communication-service/package.json b/backend/communication-service/package.json index 08fcfb3a6b..322a5b23a4 100644 --- a/backend/communication-service/package.json +++ b/backend/communication-service/package.json @@ -4,8 +4,9 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx src/server.ts", + "start": "tsx src/server.js", "dev": "tsx watch src/server.ts", + "build": "tsc", "test": "cross-env NODE_ENV=test && jest", "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." diff --git a/backend/communication-service/tsconfig.json b/backend/communication-service/tsconfig.json index 34059b779a..ce40ce2979 100644 --- a/backend/communication-service/tsconfig.json +++ b/backend/communication-service/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ + "rootDir": "./src" /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -35,7 +35,7 @@ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, + // "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ @@ -55,9 +55,9 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "noEmit": true /* Disable emitting files from a compilation. */, + // "noEmit": true, /* Disable emitting files from a compilation. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ @@ -106,5 +106,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/backend/matching-service/Dockerfile b/backend/matching-service/Dockerfile index 55b971d60b..9666970ca2 100644 --- a/backend/matching-service/Dockerfile +++ b/backend/matching-service/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-alpine AS base WORKDIR /matching-service @@ -10,4 +10,16 @@ COPY . . EXPOSE 3002 +# DEV + +FROM base AS dev + CMD ["npm", "run", "dev"] + +# PROD + +FROM base AS prod + +RUN npm run build + +CMD ["npm", "start"] diff --git a/backend/matching-service/package.json b/backend/matching-service/package.json index 7b681d8bfe..93685e7420 100644 --- a/backend/matching-service/package.json +++ b/backend/matching-service/package.json @@ -4,8 +4,9 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx src/server.ts", + "start": "tsx dist/server.js", "dev": "tsx watch src/server.ts", + "build": "tsc", "test": "cross-env NODE_ENV=test && jest", "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." diff --git a/backend/matching-service/src/app.ts b/backend/matching-service/src/app.ts index 8eb5924f6b..134f551ab0 100644 --- a/backend/matching-service/src/app.ts +++ b/backend/matching-service/src/app.ts @@ -5,7 +5,7 @@ import yaml from "yaml"; import fs from "fs"; import cors from "cors"; -import matchingRoutes from "./routes/matchingRoutes.ts"; +import matchingRoutes from "./routes/matchingRoutes"; dotenv.config(); diff --git a/backend/matching-service/src/server.ts b/backend/matching-service/src/server.ts index beb189dc0b..a53d9fdd12 100644 --- a/backend/matching-service/src/server.ts +++ b/backend/matching-service/src/server.ts @@ -1,9 +1,9 @@ import http from "http"; -import app, { allowedOrigins } from "./app.ts"; -import { handleWebsocketMatchEvents } from "./handlers/websocketHandler.ts"; +import app, { allowedOrigins } from "./app"; +import { handleWebsocketMatchEvents } from "./handlers/websocketHandler"; import { Server } from "socket.io"; -import { connectToRabbitMq } from "./config/rabbitmq.ts"; -import { verifyUserToken } from "./middlewares/basicAccessControl.ts"; +import { connectToRabbitMq } from "./config/rabbitmq"; +import { verifyUserToken } from "./middlewares/basicAccessControl"; const server = http.createServer(app); diff --git a/backend/matching-service/tsconfig.json b/backend/matching-service/tsconfig.json index ea143d1bd8..ce40ce2979 100644 --- a/backend/matching-service/tsconfig.json +++ b/backend/matching-service/tsconfig.json @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ + "rootDir": "./src" /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -35,7 +35,7 @@ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, + // "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ @@ -55,9 +55,9 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "noEmit": true /* Disable emitting files from a compilation. */, + // "noEmit": true, /* Disable emitting files from a compilation. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ @@ -106,5 +106,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/backend/qn-history-service/Dockerfile b/backend/qn-history-service/Dockerfile index 269bc6c1d7..190cee47e7 100644 --- a/backend/qn-history-service/Dockerfile +++ b/backend/qn-history-service/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-alpine AS base WORKDIR /qn-history-service @@ -10,4 +10,16 @@ COPY . . EXPOSE 3006 +# DEV + +FROM base AS dev + CMD ["npm", "run", "dev"] + +# PROD + +FROM base AS prod + +RUN npm run build + +CMD ["npm", "start"] diff --git a/backend/qn-history-service/package.json b/backend/qn-history-service/package.json index 6bbc94c2b9..ce050aa20b 100644 --- a/backend/qn-history-service/package.json +++ b/backend/qn-history-service/package.json @@ -4,8 +4,9 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx src/server.ts", + "start": "tsx dist/server.js", "dev": "tsx watch src/server.ts", + "build": "tsc", "test": "cross-env NODE_ENV=test && jest", "test:watch": "cross-env NODE_ENV=test && jest --watch", "lint": "eslint ." diff --git a/backend/qn-history-service/src/app.ts b/backend/qn-history-service/src/app.ts index 2dabadcf4f..b4004a8f14 100644 --- a/backend/qn-history-service/src/app.ts +++ b/backend/qn-history-service/src/app.ts @@ -5,7 +5,7 @@ import yaml from "yaml"; import swaggerUi from "swagger-ui-express"; import cors from "cors"; -import qnHistoryRoutes from "./routes/questionHistoryRoutes.ts"; +import qnHistoryRoutes from "./routes/questionHistoryRoutes"; dotenv.config(); diff --git a/backend/qn-history-service/src/controllers/questionHistoryController.ts b/backend/qn-history-service/src/controllers/questionHistoryController.ts index f33344179c..69e07a2ddb 100644 --- a/backend/qn-history-service/src/controllers/questionHistoryController.ts +++ b/backend/qn-history-service/src/controllers/questionHistoryController.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express"; -import QnHistory, { IQnHistory } from "../models/QnHistory.ts"; +import QnHistory, { IQnHistory } from "../models/QnHistory"; import { MONGO_OBJ_ID_FORMAT, MONGO_OBJ_ID_MALFORMED_MESSAGE, @@ -10,8 +10,8 @@ import { QN_HIST_NOT_FOUND_MESSAGE, QN_HIST_RETRIEVED_MESSAGE, SERVER_ERROR_MESSAGE, -} from "../utils/constants.ts"; -import { QnHistListParams } from "../utils/types.ts"; +} from "../utils/constants"; +import { QnHistListParams } from "../utils/types"; export const createQnHistory = async ( req: Request, diff --git a/backend/qn-history-service/src/server.ts b/backend/qn-history-service/src/server.ts index e42531d31d..9b68e4f536 100644 --- a/backend/qn-history-service/src/server.ts +++ b/backend/qn-history-service/src/server.ts @@ -1,5 +1,5 @@ -import app from "./app.ts"; -import connectDB from "./config/db.ts"; +import app from "./app"; +import connectDB from "./config/db"; const PORT = process.env.SERVICE_PORT || 3006; diff --git a/backend/qn-history-service/tsconfig.json b/backend/qn-history-service/tsconfig.json index 2d10546bb3..ce40ce2979 100644 --- a/backend/qn-history-service/tsconfig.json +++ b/backend/qn-history-service/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -25,9 +25,9 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "ESNext", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "module": "ESNext" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, + "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ @@ -35,7 +35,7 @@ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ @@ -55,9 +55,9 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "noEmit": true, /* Disable emitting files from a compilation. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ @@ -77,12 +77,12 @@ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ @@ -105,6 +105,8 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/backend/question-service/Dockerfile b/backend/question-service/Dockerfile index 958ead0382..22090cbcc7 100644 --- a/backend/question-service/Dockerfile +++ b/backend/question-service/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-alpine AS base WORKDIR /question-service @@ -10,4 +10,16 @@ COPY . . EXPOSE 3000 +# DEV + +FROM base AS dev + CMD ["npm", "run", "dev"] + +# PROD + +FROM base AS prod + +RUN npm run build + +CMD ["npm", "start"] \ No newline at end of file diff --git a/backend/question-service/package.json b/backend/question-service/package.json index 16bd165501..2733cd2ece 100644 --- a/backend/question-service/package.json +++ b/backend/question-service/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "seed": "tsx src/scripts/seed.ts", - "start": "tsx src/server.ts", + "start": "tsx dist/server.js", + "build": "tsc", "dev": "tsx watch src/server.ts", "test": "cross-env NODE_ENV=test && jest", "test:watch": "cross-env NODE_ENV=test && jest --watch", diff --git a/backend/question-service/src/app.ts b/backend/question-service/src/app.ts index 86066cbe45..d8c368f7d4 100644 --- a/backend/question-service/src/app.ts +++ b/backend/question-service/src/app.ts @@ -5,7 +5,7 @@ import yaml from "yaml"; import fs from "fs"; import cors from "cors"; -import questionRoutes from "./routes/questionRoutes.ts"; +import questionRoutes from "./routes/questionRoutes"; dotenv.config(); diff --git a/backend/question-service/src/controllers/questionController.ts b/backend/question-service/src/controllers/questionController.ts index da9dab9bd6..7309ab1072 100644 --- a/backend/question-service/src/controllers/questionController.ts +++ b/backend/question-service/src/controllers/questionController.ts @@ -1,10 +1,10 @@ import { Request, Response } from "express"; -import Question, { IQuestion } from "../models/Question.ts"; +import Question, { IQuestion } from "../models/Question"; import { checkIsExistingQuestion, getFileContent, sortAlphabetically, -} from "../utils/utils.ts"; +} from "../utils/utils"; import { DUPLICATE_QUESTION_MESSAGE, QN_DESC_EXCEED_CHAR_LIMIT_MESSAGE, @@ -19,11 +19,11 @@ import { CATEGORIES_RETRIEVED_MESSAGE, MONGO_OBJ_ID_FORMAT, MONGO_OBJ_ID_MALFORMED_MESSAGE, -} from "../utils/constants.ts"; +} from "../utils/constants"; -import { upload, uploadTestcaseFiles } from "../config/multer.ts"; +import { upload, uploadTestcaseFiles } from "../config/multer"; import { uploadFileToFirebase } from "../utils/utils"; -import { QnListSearchFilterParams, RandomQnCriteria } from "../utils/types.ts"; +import { QnListSearchFilterParams, RandomQnCriteria } from "../utils/types"; const FIREBASE_TESTCASE_FILES_FOLDER_NAME = "testcaseFiles/"; diff --git a/backend/question-service/src/routes/questionRoutes.ts b/backend/question-service/src/routes/questionRoutes.ts index b2d5f1c949..35c1ac39b3 100644 --- a/backend/question-service/src/routes/questionRoutes.ts +++ b/backend/question-service/src/routes/questionRoutes.ts @@ -9,8 +9,8 @@ import { readCategories, readRandomQuestion, createFileLink, -} from "../controllers/questionController.ts"; -import { verifyAdminToken } from "../middlewares/basicAccessControl.ts"; +} from "../controllers/questionController"; +import { verifyAdminToken } from "../middlewares/basicAccessControl"; const router = express.Router(); diff --git a/backend/question-service/src/server.ts b/backend/question-service/src/server.ts index 513945f03e..704d923fdc 100644 --- a/backend/question-service/src/server.ts +++ b/backend/question-service/src/server.ts @@ -1,5 +1,5 @@ -import app from "./app.ts"; -import connectDB from "./config/db.ts"; +import app from "./app"; +import connectDB from "./config/db"; const PORT = process.env.SERVICE_PORT || 3000; diff --git a/backend/question-service/tsconfig.json b/backend/question-service/tsconfig.json index 26e7f89381..b7501d7a18 100644 --- a/backend/question-service/tsconfig.json +++ b/backend/question-service/tsconfig.json @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ + "rootDir": "./src" /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -35,12 +35,12 @@ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, + // "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - "resolveJsonModule": true, /* Enable importing .json files. */ + "resolveJsonModule": true /* Enable importing .json files. */, // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ @@ -55,9 +55,9 @@ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "noEmit": true /* Disable emitting files from a compilation. */, + // "noEmit": true /* Disable emitting files from a compilation. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ @@ -106,5 +106,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] } diff --git a/backend/user-service/Dockerfile b/backend/user-service/Dockerfile index 8fc18a9843..550b015a95 100644 --- a/backend/user-service/Dockerfile +++ b/backend/user-service/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-alpine as base WORKDIR /user-service @@ -10,4 +10,16 @@ COPY . . EXPOSE 3001 +# DEV + +FROM base AS dev + CMD ["npm", "run", "dev"] + +# PROD + +FROM base AS prod + +RUN npm run build + +CMD ["npm", "start"] diff --git a/backend/user-service/package.json b/backend/user-service/package.json index 0cdabebd0f..aef24b7c49 100644 --- a/backend/user-service/package.json +++ b/backend/user-service/package.json @@ -5,8 +5,9 @@ "main": "app.ts", "type": "module", "scripts": { - "start": "tsx ./src/server.ts", - "dev": "tsx watch ./src/server.ts", + "start": "tsx dist/server.js", + "dev": "tsx watch src/server.ts", + "build": "tsc", "lint": "eslint .", "test": "cross-env NODE_ENV=test jest --detectOpenHandles", "test:watch": "cross-env NODE_ENV=test jest --watch --detectOpenHandles" diff --git a/backend/user-service/src/server.ts b/backend/user-service/src/server.ts index 606e155603..d838149671 100644 --- a/backend/user-service/src/server.ts +++ b/backend/user-service/src/server.ts @@ -1,9 +1,9 @@ import http from "http"; -import index from "./app.ts"; +import index from "./app"; import dotenv from "dotenv"; -import { connectToDB } from "./model/repository.ts"; -import { seedAdminAccount } from "./scripts/seed.ts"; -import { connectRedis } from "./config/redis.ts"; +import { connectToDB } from "./model/repository"; +import { seedAdminAccount } from "./scripts/seed"; +import { connectRedis } from "./config/redis"; dotenv.config(); diff --git a/backend/user-service/tsconfig.json b/backend/user-service/tsconfig.json index 34059b779a..972bb166b1 100644 --- a/backend/user-service/tsconfig.json +++ b/backend/user-service/tsconfig.json @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ + "rootDir": "./src" /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -35,7 +35,7 @@ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, + // "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */, // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ @@ -53,11 +53,11 @@ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "noEmit": true /* Disable emitting files from a compilation. */, + // "noEmit": true /* Disable emitting files from a compilation. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ @@ -106,5 +106,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "exclude": ["node_modules"], + "include": ["src/**/*"] } diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 0000000000..1e713ecc48 --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,266 @@ +services: + user-service: + image: peerprep/user-service + build: + context: ./backend/user-service + dockerfile: Dockerfile + target: prod + env_file: ./backend/user-service/.env + ports: + - 3001:3001 + depends_on: + - user-service-mongo + - user-service-redis + networks: + - peerprep-network + restart: on-failure + + question-service: + image: peerprep/question-service + build: + context: ./backend/question-service + dockerfile: Dockerfile + target: prod + env_file: ./backend/question-service/.env + ports: + - 3000:3000 + depends_on: + - question-service-mongo + - user-service + networks: + - peerprep-network + restart: on-failure + + matching-service: + image: peerprep/matching-service + build: + context: ./backend/matching-service + dockerfile: Dockerfile + target: prod + env_file: ./backend/matching-service/.env + ports: + - 3002:3002 + depends_on: + rabbitmq: + condition: service_healthy + user-service: + condition: service_started + networks: + - peerprep-network + restart: on-failure + + collab-service: + image: peerprep/collab-service + build: + context: ./backend/collab-service + dockerfile: Dockerfile + target: prod + env_file: ./backend/collab-service/.env + ports: + - 3003:3003 + depends_on: + - collab-service-redis + networks: + - peerprep-network + restart: on-failure + + code-execution-service: + image: peerprep/code-execution-service + build: + context: ./backend/code-execution-service + dockerfile: Dockerfile + target: prod + env_file: ./backend/code-execution-service/.env + ports: + - 3004:3004 + networks: + - peerprep-network + restart: on-failure + + communication-service: + image: peerprep/communication-service + build: + context: ./backend/communication-service + dockerfile: Dockerfile + target: prod + env_file: ./backend/communication-service/.env + ports: + - 3005:3005 + depends_on: + - user-service + networks: + - peerprep-network + restart: on-failure + + qn-history-service: + image: peerprep/qn-history-service + build: + context: ./backend/qn-history-service + dockerfile: Dockerfile + target: prod + env_file: ./backend/qn-history-service/.env + ports: + - 3006:3006 + depends_on: + - qn-history-service-mongo + - user-service + - question-service + networks: + - peerprep-network + restart: on-failure + + # frontend: + # image: peerprep/frontend + # build: ./frontend + # ports: + # - 5173:5173 + # depends_on: + # - user-service + # # - question-service + # # - matching-service + # # - collab-service + # # - code-execution-service + # # - communication-service + # # - qn-history-service + # networks: + # - peerprep-network + # restart: on-failure + + question-service-mongo: + image: mongo + restart: always + ports: + - 27017:27017 + networks: + - peerprep-network + volumes: + - question-service-mongo-data:/data/db + env_file: + - ./backend/question-service/.env + + question-service-mongo-express: + image: mongo-express + restart: always + ports: + - 8081:8081 + networks: + - peerprep-network + depends_on: + - question-service-mongo + env_file: ./backend/question-service/.env + + user-service-mongo: + image: mongo + restart: always + ports: + - 27018:27017 + networks: + - peerprep-network + volumes: + - user-service-mongo-data:/data/db + env_file: + - ./backend/user-service/.env + + user-service-mongo-express: + image: mongo-express + restart: always + ports: + - 8082:8081 + networks: + - peerprep-network + depends_on: + - user-service-mongo + env_file: ./backend/user-service/.env + + qn-history-service-mongo: + image: mongo + restart: always + ports: + - 27019:27017 + networks: + - peerprep-network + volumes: + - qn-history-service-mongo-data:/data/db + env_file: + - ./backend/qn-history-service/.env + + qn-history-service-mongo-express: + image: mongo-express + restart: always + ports: + - 8083:8081 + networks: + - peerprep-network + depends_on: + - qn-history-service-mongo + env_file: ./backend/qn-history-service/.env + + rabbitmq: + image: rabbitmq:4.0-management + container_name: rabbitmq + restart: always + ports: + - 5672:5672 + - 15672:15672 + networks: + - peerprep-network + env_file: ./backend/matching-service/.env + healthcheck: + test: rabbitmq-diagnostics check_port_connectivity + interval: 20s + timeout: 10s + retries: 10 + + user-service-redis: + image: redis:8.0-M01 + container_name: user-service-redis + ports: + - 6379:6379 + networks: + - peerprep-network + volumes: + - user-service-redis-data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + interval: 10s + timeout: 10s + retries: 10 + command: ["redis-server"] + + collab-service-redis: + image: redis:8.0-M01 + container_name: collab-service-redis + ports: + - 6380:6379 + networks: + - peerprep-network + volumes: + - collab-service-redis-data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + interval: 10s + timeout: 10s + retries: 10 + command: ["redis-server"] + + redis-insight: + image: redis/redisinsight:latest + container_name: redis-insight + ports: + - 5540:5540 + networks: + - peerprep-network + volumes: + - redis-insight-data:/data + +volumes: + question-service-mongo-data: + user-service-mongo-data: + qn-history-service-mongo-data: + user-service-redis-data: + collab-service-redis-data: + redis-insight-data: + +networks: + peerprep-network: + driver: bridge diff --git a/docker-compose-test.yml b/docker-compose-test.yml index d6bbbf78a7..450447edb0 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -3,7 +3,10 @@ name: peerprep-test services: test-user-service: image: peerprep/user-service - build: ./backend/user-service + build: + context: ./backend/user-service + dockerfile: Dockerfile + target: dev environment: - NODE_ENV=test - SERVICE_PORT=3001 @@ -32,7 +35,10 @@ services: test-question-service: image: peerprep/question-service - build: ./backend/question-service + build: + context: ./backend/question-service + dockerfile: Dockerfile + target: dev environment: - NODE_ENV=test - SERVICE_PORT=3000 @@ -53,7 +59,10 @@ services: test-matching-service: image: peerprep/matching-service - build: ./backend/matching-service + build: + context: ./backend/matching-service + dockerfile: Dockerfile + target: dev environment: - NODE_ENV=test - SERVICE_PORT=3002 @@ -70,7 +79,10 @@ services: test-collab-service: image: peerprep/collab-service - build: ./backend/collab-service + build: + context: ./backend/collab-service + dockerfile: Dockerfile + target: dev environment: - NODE_ENV=test - SERVICE_PORT=3003 @@ -87,7 +99,10 @@ services: test-code-execution-service: image: peerprep/code-execution-service - build: ./backend/code-execution-service + build: + context: ./backend/code-execution-service + dockerfile: Dockerfile + target: dev environment: - NODE_ENV=test - SERVICE_PORT=3004 @@ -102,7 +117,10 @@ services: test-communication-service: image: peerprep/communication-service - build: ./backend/communication-service + build: + context: ./backend/communication-service + dockerfile: Dockerfile + target: dev environment: - NODE_ENV=test - SERVICE_PORT=3005 @@ -116,7 +134,10 @@ services: test-qn-history-service: image: peerprep/qn-history-service - build: ./backend/qn-history-service + build: + context: ./backend/qn-history-service + dockerfile: Dockerfile + target: dev environment: - NODE_ENV=test - SERVICE_PORT=3006 @@ -133,7 +154,10 @@ services: test-frontend: image: peerprep/frontend - build: ./frontend + build: + context: ./frontend + dockerfile: Dockerfile + target: dev networks: - peerprep-network volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 1d3067ba03..ccdad14888 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,10 @@ services: user-service: image: peerprep/user-service - build: ./backend/user-service + build: + context: ./backend/user-service + dockerfile: Dockerfile + target: dev environment: - CHOKIDAR_USEPOLLING=true env_file: ./backend/user-service/.env @@ -19,7 +22,10 @@ services: question-service: image: peerprep/question-service - build: ./backend/question-service + build: + context: ./backend/question-service + dockerfile: Dockerfile + target: dev environment: - CHOKIDAR_USEPOLLING=true env_file: ./backend/question-service/.env @@ -37,7 +43,10 @@ services: matching-service: image: peerprep/matching-service - build: ./backend/matching-service + build: + context: ./backend/matching-service + dockerfile: Dockerfile + target: dev environment: - CHOKIDAR_USEPOLLING=true env_file: ./backend/matching-service/.env @@ -57,7 +66,10 @@ services: collab-service: image: peerprep/collab-service - build: ./backend/collab-service + build: + context: ./backend/collab-service + dockerfile: Dockerfile + target: dev environment: - CHOKIDAR_USEPOLLING=true env_file: ./backend/collab-service/.env @@ -74,7 +86,10 @@ services: code-execution-service: image: peerprep/code-execution-service - build: ./backend/code-execution-service + build: + context: ./backend/code-execution-service + dockerfile: Dockerfile + target: dev environment: - CHOKIDAR_USEPOLLING=true env_file: ./backend/code-execution-service/.env @@ -89,7 +104,10 @@ services: communication-service: image: peerprep/communication-service - build: ./backend/communication-service + build: + context: ./backend/communication-service + dockerfile: Dockerfile + target: dev environment: - CHOKIDAR_USEPOLLING=true env_file: ./backend/communication-service/.env @@ -106,7 +124,10 @@ services: qn-history-service: image: peerprep/qn-history-service - build: ./backend/qn-history-service + build: + context: ./backend/qn-history-service + dockerfile: Dockerfile + target: dev environment: - CHOKIDAR_USEPOLLING=true env_file: ./backend/qn-history-service/.env @@ -125,7 +146,10 @@ services: frontend: image: peerprep/frontend - build: ./frontend + build: + context: ./frontend + dockerfile: Dockerfile + target: dev environment: - CHOKIDAR_USEPOLLING=true ports: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 7888b16163..eeadf85bd5 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-alpine as base WORKDIR /frontend @@ -10,4 +10,16 @@ COPY . . EXPOSE 5173 -CMD ["npm", "run", "dev"] \ No newline at end of file +# DEV + +FROM base AS dev + +CMD ["npm", "run", "dev"] + +# PROD + +FROM base AS prod + +RUN npm run build + +CMD ["npm", "run", "preview"] diff --git a/frontend/src/components/Navbar/Navbar.test.tsx b/frontend/src/components/Navbar/Navbar.test.tsx index 42a7dccb93..9779ef9fd4 100644 --- a/frontend/src/components/Navbar/Navbar.test.tsx +++ b/frontend/src/components/Navbar/Navbar.test.tsx @@ -21,6 +21,7 @@ beforeEach(() => { jest.spyOn(matchHooks, "useMatch").mockImplementation(() => ({ findMatch: jest.fn(), stopMatch: () => mockUseNavigate("/home"), + getMatchId: jest.fn(), acceptMatch: jest.fn(), rematch: jest.fn(), retryMatch: jest.fn(), @@ -32,6 +33,8 @@ beforeEach(() => { partner: null, matchPending: false, loading: false, + questionId: "123", + questionTitle: "Question", })); }); diff --git a/frontend/src/components/ProfileDetails/ProfileDetailstest.tsx b/frontend/src/components/ProfileDetails/ProfileDetails.test.tsx similarity index 100% rename from frontend/src/components/ProfileDetails/ProfileDetailstest.tsx rename to frontend/src/components/ProfileDetails/ProfileDetails.test.tsx From 73b443900418032a36869e70b16e2cce7d15a576 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 10 Nov 2024 18:06:35 +0800 Subject: [PATCH 162/192] Add build script --- .gitignore | 1 + docker-compose-prod.yml | 35 +++++++++++++++++++---------------- frontend/Dockerfile | 6 ++++-- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 84f98a5642..7d862a8996 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +*.tsbuildinfo # Coverage coverage diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index 1e713ecc48..a22235fee5 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -109,22 +109,25 @@ services: - peerprep-network restart: on-failure - # frontend: - # image: peerprep/frontend - # build: ./frontend - # ports: - # - 5173:5173 - # depends_on: - # - user-service - # # - question-service - # # - matching-service - # # - collab-service - # # - code-execution-service - # # - communication-service - # # - qn-history-service - # networks: - # - peerprep-network - # restart: on-failure + frontend: + image: peerprep/frontend + build: + context: ./frontend + dockerfile: Dockerfile + target: prod + ports: + - 5173:4173 + depends_on: + - user-service + - question-service + - matching-service + - collab-service + - code-execution-service + - communication-service + - qn-history-service + networks: + - peerprep-network + restart: on-failure question-service-mongo: image: mongo diff --git a/frontend/Dockerfile b/frontend/Dockerfile index eeadf85bd5..a78b37563e 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -8,12 +8,12 @@ RUN npm ci COPY . . -EXPOSE 5173 - # DEV FROM base AS dev +EXPOSE 5173 + CMD ["npm", "run", "dev"] # PROD @@ -22,4 +22,6 @@ FROM base AS prod RUN npm run build +EXPOSE 4173 + CMD ["npm", "run", "preview"] From 241114f997d8218f619d11923764543d6dcfdd09 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Sun, 10 Nov 2024 19:34:28 +0800 Subject: [PATCH 163/192] Add build step --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3978637ca8..d6f20f924a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,8 @@ jobs: run: npm run lint - name: Test run: docker compose -f docker-compose-test.yml run --rm test-frontend + - name: Build + run: docker compose -f docker-compose-prod.yml build frontend backend-ci: runs-on: ubuntu-latest strategy: @@ -65,3 +67,5 @@ jobs: JWT_SECRET: ${{ secrets.JWT_SECRET }} ONE_COMPILER_KEY: ${{ secrets.ONE_COMPILER_KEY }} run: docker compose -f docker-compose-test.yml run --rm test-${{ matrix.service }} + - name: Build + run: docker compose -f docker-compose-prod.yml build ${{ matrix.service }} From 3f9359cc60ea1ba56fa79ffa42c8d71ab9f26158 Mon Sep 17 00:00:00 2001 From: jolynloh Date: Mon, 11 Nov 2024 00:23:57 +0800 Subject: [PATCH 164/192] Add response check --- .../qn-history-service/src/middlewares/basicAccessControl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/qn-history-service/src/middlewares/basicAccessControl.ts b/backend/qn-history-service/src/middlewares/basicAccessControl.ts index 1053b25a53..04aaea9930 100644 --- a/backend/qn-history-service/src/middlewares/basicAccessControl.ts +++ b/backend/qn-history-service/src/middlewares/basicAccessControl.ts @@ -12,6 +12,6 @@ export const verifyToken = ( .then(() => next()) .catch((err) => { console.log(err.response); - return res.status(err.response.status).json(err.response.data); + return res.status(err.response?.status).json(err.response?.data); }); }; From 81a87f896bb35753cdae09674f7ea1c5b9b0f361 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 11 Nov 2024 14:43:04 +0800 Subject: [PATCH 165/192] Refactor --- backend/matching-service/package-lock.json | 34 +---- backend/matching-service/package.json | 4 +- backend/matching-service/src/app.ts | 10 -- .../matching-service/src/config/rabbitmq.ts | 117 ++++++++++------ .../src/handlers/matchHandler.ts | 125 +++++++++--------- .../src/handlers/websocketHandler.ts | 9 +- .../src/middlewares/basicAccessControl.ts | 1 - .../src/routes/matchingRoutes.ts | 5 - .../src/utils/messageQueue.ts | 46 ------- backend/matching-service/src/utils/types.ts | 21 +++ backend/matching-service/swagger.yml | 5 - package-lock.json | 2 +- 12 files changed, 171 insertions(+), 208 deletions(-) delete mode 100644 backend/matching-service/src/routes/matchingRoutes.ts delete mode 100644 backend/matching-service/src/utils/messageQueue.ts create mode 100644 backend/matching-service/src/utils/types.ts delete mode 100644 backend/matching-service/swagger.yml diff --git a/backend/matching-service/package-lock.json b/backend/matching-service/package-lock.json index ce7092cf4d..8a542f887d 100644 --- a/backend/matching-service/package-lock.json +++ b/backend/matching-service/package-lock.json @@ -15,9 +15,7 @@ "dotenv": "^16.4.5", "express": "^4.21.1", "socket.io": "^4.8.0", - "swagger-ui-express": "^5.0.1", - "uuid": "^10.0.0", - "yaml": "^2.5.1" + "uuid": "^10.0.0" }, "devDependencies": { "@eslint/js": "^9.12.0", @@ -6569,25 +6567,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swagger-ui-dist": { - "version": "5.17.14", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", - "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==" - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -7082,17 +7061,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/backend/matching-service/package.json b/backend/matching-service/package.json index 7b681d8bfe..34962bde25 100644 --- a/backend/matching-service/package.json +++ b/backend/matching-service/package.json @@ -19,9 +19,7 @@ "dotenv": "^16.4.5", "express": "^4.21.1", "socket.io": "^4.8.0", - "swagger-ui-express": "^5.0.1", - "uuid": "^10.0.0", - "yaml": "^2.5.1" + "uuid": "^10.0.0" }, "devDependencies": { "@eslint/js": "^9.12.0", diff --git a/backend/matching-service/src/app.ts b/backend/matching-service/src/app.ts index 8eb5924f6b..309e97485d 100644 --- a/backend/matching-service/src/app.ts +++ b/backend/matching-service/src/app.ts @@ -1,28 +1,18 @@ import express, { Request, Response } from "express"; import dotenv from "dotenv"; -import swaggerUi from "swagger-ui-express"; -import yaml from "yaml"; -import fs from "fs"; import cors from "cors"; -import matchingRoutes from "./routes/matchingRoutes.ts"; - dotenv.config(); export const allowedOrigins = process.env.ORIGINS ? process.env.ORIGINS.split(",") : ["http://localhost:5173", "http://127.0.0.1:5173"]; -const file = fs.readFileSync("./swagger.yml", "utf-8"); -const swaggerDocument = yaml.parse(file); - const app = express(); app.use(cors({ origin: allowedOrigins, credentials: true })); app.options("*", cors({ origin: allowedOrigins, credentials: true })); -app.use("/api/matching", matchingRoutes); -app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); app.get("/", (req: Request, res: Response) => { res.status(200).json({ message: "Hello world from matching service" }); }); diff --git a/backend/matching-service/src/config/rabbitmq.ts b/backend/matching-service/src/config/rabbitmq.ts index 0623f88b02..057d9755af 100644 --- a/backend/matching-service/src/config/rabbitmq.ts +++ b/backend/matching-service/src/config/rabbitmq.ts @@ -1,63 +1,82 @@ import amqplib, { Connection } from "amqplib"; import dotenv from "dotenv"; -import { matchUsers } from "../utils/messageQueue"; -import { MatchRequestItem } from "../handlers/matchHandler"; +import { matchUsers } from "../handlers/matchHandler"; import { Complexities, Categories, Languages } from "../utils/constants"; +import { MatchRequest, MatchRequestItem } from "../utils/types"; dotenv.config(); const RABBITMQ_ADDR = process.env.RABBITMQ_ADDR || "amqp://localhost:5672"; +const QUEUE_NAME_DELIMITER = "_"; let mrConnection: Connection; -const queues: string[] = []; -const pendingQueueRequests = new Map>(); +const waitingLists = new Map>(); -const initQueueNames = () => { - for (const complexity of Object.values(Complexities)) { - for (const category of Object.values(Categories)) { - for (const language of Object.values(Languages)) { - queues.push(`${complexity}_${category}_${language}`); - } +export const connectToRabbitMq = async () => { + try { + mrConnection = await amqplib.connect(RABBITMQ_ADDR); + const queues = setUpQueueNames(); + for (const queue of queues) { + await setUpConsumer(queue); + getWaitingList(queue); } + } catch (error) { + console.error(error); + process.exit(1); } }; -const setUpQueue = async (queueName: string) => { +export const sendToProducer = async ( + matchRequest: MatchRequest, + requestId: string, + rejectedPartnerId?: string +): Promise => { + const { user, complexity, category, language, timeout } = matchRequest; + + const requestItem: MatchRequestItem = { + id: requestId, + user: user, + sentTimestamp: Date.now(), + ttlInSecs: timeout, + rejectedPartnerId: rejectedPartnerId, + }; + + const sent = await routeToQueue( + [complexity, category, language], + requestItem + ); + return sent; +}; + +const setUpConsumer = async (queueName: string) => { const consumerChannel = await mrConnection.createChannel(); - await consumerChannel.assertQueue(queueName); + await consumerChannel.assertQueue(queueName, { durable: true }); consumerChannel.consume(queueName, (msg) => { if (msg !== null) { - matchUsers(queueName, msg.content.toString()); + const matchRequestItem = JSON.parse(msg.content.toString()); + const waitingList = getWaitingList(queueName); + const [complexity, category] = deconstructQueueName(queueName); + matchUsers(matchRequestItem, waitingList, complexity, category); consumerChannel.ack(msg); } }); }; -export const connectToRabbitMq = async () => { - try { - initQueueNames(); - mrConnection = await amqplib.connect(RABBITMQ_ADDR); - for (const queue of queues) { - await setUpQueue(queue); - pendingQueueRequests.set(queue, new Map()); - } - } catch (error) { - console.error(error); - process.exit(1); - } -}; - -export const sendToQueue = async ( - complexity: string, - category: string, - language: string, - data: MatchRequestItem +const routeToQueue = async ( + criterias: string[], + requestItem: MatchRequestItem ): Promise => { try { - const queueName = `${complexity}_${category}_${language}`; + const queueName = constructQueueName(criterias); const senderChannel = await mrConnection.createChannel(); - senderChannel.sendToQueue(queueName, Buffer.from(JSON.stringify(data))); + senderChannel.sendToQueue( + queueName, + Buffer.from(JSON.stringify(requestItem)), + { + persistent: true, + } + ); return true; } catch (error) { console.log(error); @@ -65,8 +84,30 @@ export const sendToQueue = async ( } }; -export const getPendingRequests = ( - queueName: string -): Map => { - return pendingQueueRequests.get(queueName)!; +const setUpQueueNames = () => { + const queues = []; + for (const complexity of Object.values(Complexities)) { + for (const category of Object.values(Categories)) { + for (const language of Object.values(Languages)) { + const queueName = constructQueueName([complexity, category, language]); + queues.push(queueName); + } + } + } + return queues; +}; + +const constructQueueName = (criterias: string[]) => { + return criterias.join(QUEUE_NAME_DELIMITER); +}; + +const deconstructQueueName = (queueName: string) => { + return queueName.split(QUEUE_NAME_DELIMITER); +}; + +const getWaitingList = (queueName: string): Map => { + if (!waitingLists.has(queueName)) { + waitingLists.set(queueName, new Map()); + } + return waitingLists.get(queueName)!; }; diff --git a/backend/matching-service/src/handlers/matchHandler.ts b/backend/matching-service/src/handlers/matchHandler.ts index f8cdbbc939..54beebfaa1 100644 --- a/backend/matching-service/src/handlers/matchHandler.ts +++ b/backend/matching-service/src/handlers/matchHandler.ts @@ -1,6 +1,10 @@ import { v4 as uuidv4 } from "uuid"; -import { sendToQueue } from "../config/rabbitmq"; -import { sendMatchFound } from "./websocketHandler"; +import { + isActiveRequest, + isUserConnected, + sendMatchFound, +} from "./websocketHandler"; +import { MatchRequestItem, MatchUser } from "../utils/types"; interface Match { matchUser1: MatchUser; @@ -8,73 +12,49 @@ interface Match { accepted: boolean; complexity: string; category: string; - language: string; -} - -export interface MatchUser { - id: string; - username: string; - profile?: string; -} - -export interface MatchRequest { - user: MatchUser; - complexity: string; - category: string; - language: string; - timeout: number; -} - -export interface MatchRequestItem { - id: string; - user: MatchUser; - sentTimestamp: number; - ttlInSecs: number; - rejectedPartnerId?: string; } const matches = new Map(); -export const sendMatchRequest = async ( - matchRequest: MatchRequest, - requestId: string, - rejectedPartnerId?: string -): Promise => { - const { user, complexity, category, language, timeout } = matchRequest; - - const matchItem: MatchRequestItem = { - id: requestId, - user: user, - sentTimestamp: Date.now(), - ttlInSecs: timeout, - rejectedPartnerId: rejectedPartnerId, - }; - - const sent = await sendToQueue(complexity, category, language, matchItem); - return sent; -}; - -export const createMatch = ( - requestItem1: MatchRequestItem, - requestItem2: MatchRequestItem, +export const matchUsers = ( + newRequest: MatchRequestItem, + waitingList: Map, complexity: string, - category: string, - language: string + category: string ) => { - const matchId = uuidv4(); - const matchUser1 = requestItem1.user; - const matchUser2 = requestItem2.user; + const newRequestUid = newRequest.user.id; + + for (const [uid, waitListRequest] of waitingList) { + if ( + isExpired(waitListRequest) || + !isUserConnected(uid) || + !isActiveRequest(uid, waitListRequest.id) || + uid === newRequestUid + ) { + waitingList.delete(uid); + continue; + } - matches.set(matchId, { - matchUser1: matchUser1, - matchUser2: matchUser2, - accepted: false, - complexity, - category, - language, - }); + if ( + isExpired(newRequest) || + !isUserConnected(newRequestUid) || + !isActiveRequest(newRequestUid, newRequest.id) + ) { + return; + } - sendMatchFound(matchId, matchUser1, matchUser2); + if ( + uid === newRequest.rejectedPartnerId || + newRequestUid === waitListRequest.rejectedPartnerId + ) { + continue; + } + + waitingList.delete(uid); + createMatch(waitListRequest.user, newRequest.user, complexity, category); + return; + } + waitingList.set(newRequestUid, newRequest); }; export const handleMatchAccept = (matchId: string): boolean => { @@ -116,3 +96,26 @@ export const getMatchByUid = ( export const getMatchById = (matchId: string): Match | undefined => { return matches.get(matchId); }; + +const createMatch = ( + matchUser1: MatchUser, + matchUser2: MatchUser, + complexity: string, + category: string +) => { + const matchId = uuidv4(); + + matches.set(matchId, { + matchUser1: matchUser1, + matchUser2: matchUser2, + accepted: false, + complexity, + category, + }); + + sendMatchFound(matchId, matchUser1, matchUser2); +}; + +const isExpired = (data: MatchRequestItem): boolean => { + return Date.now() - data.sentTimestamp >= data.ttlInSecs * 1000; +}; diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index 92f9861836..e06eee07a7 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -1,17 +1,16 @@ import { Socket } from "socket.io"; import { - sendMatchRequest, handleMatchAccept, - MatchRequest, handleMatchDelete, getMatchIdByUid, - MatchUser, getMatchByUid, getMatchById, } from "./matchHandler"; import { io } from "../server"; import { v4 as uuidv4 } from "uuid"; import { getRandomQuestion } from "../api/questionService"; +import { MatchRequest, MatchUser } from "../utils/types"; +import { sendToProducer } from "../config/rabbitmq"; enum MatchEvents { // Receive @@ -105,7 +104,7 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { requestId: requestId, }); - const sent = await sendMatchRequest(matchRequest, requestId); + const sent = await sendToProducer(matchRequest, requestId); if (!sent) { socket.emit(MatchEvents.MATCH_REQUEST_ERROR); userConnections.delete(uid); @@ -170,7 +169,7 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { requestId: requestId, }); - const sent = await sendMatchRequest(rematchRequest, requestId, partnerId); + const sent = await sendToProducer(rematchRequest, requestId, partnerId); if (!sent) { socket.emit(MatchEvents.MATCH_REQUEST_ERROR); } diff --git a/backend/matching-service/src/middlewares/basicAccessControl.ts b/backend/matching-service/src/middlewares/basicAccessControl.ts index 15088e9a86..a58e7314c9 100644 --- a/backend/matching-service/src/middlewares/basicAccessControl.ts +++ b/backend/matching-service/src/middlewares/basicAccessControl.ts @@ -9,7 +9,6 @@ export const verifyUserToken = ( socket.handshake.headers.authorization || socket.handshake.auth.token; verifyToken(token) .then(() => { - console.log("Valid credentials"); next(); }) .catch((err) => { diff --git a/backend/matching-service/src/routes/matchingRoutes.ts b/backend/matching-service/src/routes/matchingRoutes.ts deleted file mode 100644 index 9da7196e6e..0000000000 --- a/backend/matching-service/src/routes/matchingRoutes.ts +++ /dev/null @@ -1,5 +0,0 @@ -import express from "express"; - -const router = express.Router(); - -export default router; diff --git a/backend/matching-service/src/utils/messageQueue.ts b/backend/matching-service/src/utils/messageQueue.ts deleted file mode 100644 index d1781d1c5c..0000000000 --- a/backend/matching-service/src/utils/messageQueue.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { getPendingRequests } from "../config/rabbitmq"; -import { createMatch, MatchRequestItem } from "../handlers/matchHandler"; -import { isActiveRequest, isUserConnected } from "../handlers/websocketHandler"; - -export const matchUsers = (queueName: string, newRequest: string) => { - const pendingRequests = getPendingRequests(queueName); - const newRequestJson = JSON.parse(newRequest) as MatchRequestItem; - const newRequestUid = newRequestJson.user.id; - const [complexity, category, language] = queueName.split("_"); - - for (const [uid, pendingRequest] of pendingRequests) { - if ( - isExpired(pendingRequest) || - !isUserConnected(uid) || - !isActiveRequest(uid, pendingRequest.id) || - uid === newRequestUid - ) { - pendingRequests.delete(uid); - continue; - } - - if ( - isExpired(newRequestJson) || - !isUserConnected(newRequestUid) || - !isActiveRequest(newRequestUid, newRequestJson.id) - ) { - return; - } - - if ( - uid === newRequestJson.rejectedPartnerId || - newRequestUid === pendingRequest.rejectedPartnerId - ) { - continue; - } - - pendingRequests.delete(uid); - createMatch(pendingRequest, newRequestJson, complexity, category, language); - return; - } - pendingRequests.set(newRequestUid, newRequestJson); -}; - -const isExpired = (data: MatchRequestItem): boolean => { - return Date.now() - data.sentTimestamp >= data.ttlInSecs * 1000; -}; diff --git a/backend/matching-service/src/utils/types.ts b/backend/matching-service/src/utils/types.ts new file mode 100644 index 0000000000..d6217c3a93 --- /dev/null +++ b/backend/matching-service/src/utils/types.ts @@ -0,0 +1,21 @@ +export interface MatchUser { + id: string; + username: string; + profile?: string; +} + +export interface MatchRequest { + user: MatchUser; + complexity: string; + category: string; + language: string; + timeout: number; +} + +export interface MatchRequestItem { + id: string; + user: MatchUser; + sentTimestamp: number; + ttlInSecs: number; + rejectedPartnerId?: string; +} diff --git a/backend/matching-service/swagger.yml b/backend/matching-service/swagger.yml deleted file mode 100644 index 440578422a..0000000000 --- a/backend/matching-service/swagger.yml +++ /dev/null @@ -1,5 +0,0 @@ -openapi: 3.0.0 - -info: - title: Matching Service - version: 1.0.0 diff --git a/package-lock.json b/package-lock.json index 568722e9a9..62d7c2f092 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "peerprep", + "name": "peer-prep", "lockfileVersion": 3, "requires": true, "packages": { From e5be5239973f7c3364a6721c788df039534258eb Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Mon, 11 Nov 2024 16:57:09 +0800 Subject: [PATCH 166/192] Update readme --- backend/matching-service/README.md | 53 +++++++++++++++++- .../docs/images/postman-setup1.png | Bin 0 -> 49363 bytes .../docs/images/postman-setup2.png | Bin 0 -> 34894 bytes .../docs/images/postman-setup3.png | Bin 0 -> 29937 bytes .../docs/images/postman-setup4.png | Bin 0 -> 25847 bytes .../src/handlers/websocketHandler.ts | 8 +-- .../CollabSessionControls/index.tsx | 2 + frontend/src/contexts/MatchContext.tsx | 6 +- frontend/src/pages/NewQuestion/index.tsx | 2 + 9 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 backend/matching-service/docs/images/postman-setup1.png create mode 100644 backend/matching-service/docs/images/postman-setup2.png create mode 100644 backend/matching-service/docs/images/postman-setup3.png create mode 100644 backend/matching-service/docs/images/postman-setup4.png diff --git a/backend/matching-service/README.md b/backend/matching-service/README.md index 597f6f26a4..ea095a5565 100644 --- a/backend/matching-service/README.md +++ b/backend/matching-service/README.md @@ -28,6 +28,55 @@ ## After running -1. To view Matching Service documentation, go to http://localhost:3002/docs. +1. Using applications like Postman, you can interact with the Matching Service on port 3002. If you wish to change this, please update the `.env` file. -2. Using applications like Postman, you can interact with the Matching Service on port 3002. If you wish to change this, please update the `.env` file. +2. Setting up Socket.IO connection on Postman: + + - You should open 2 tabs on Postman to simulate 2 users in the Matching Service. + + - Select the `Socket.IO` option and set URL to `http://localhost:3002`. Click `Connect`. + + ![image1.png](./docs/images/postman-setup1.png) + + - Add the following events in the `Events` tab and listen to them. + + ![image2.png](./docs/images/postman-setup2.png) + + - In the `Headers` tab, add a valid JWT token in the `Authorization` header. + + ![image3.png](./docs/images/postman-setup3.png) + + - In the `Message` tab, select `JSON` in the bottom left dropdown to ensure that your message is being parsed correctly. In the `Event name` input field, enter the name of the event you would like to send a message to. Click on `Send` to send a message. + + ![image4.png](./docs/images/postman-setup4.png) + +## Events Available + +| Event Name | Description | Parameters | Response Event | +| ------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **user_connected** | User joins the matching process | `uid` (string): ID of the user. | None | +| **user_disconnected** | User leaves the matching process | `uid` (string): ID of the user. | **match_unsuccessful**: If the user left during the match offer phase, notify the partner user that the match was unsuccessful | +| **match_request** | Sends a match request | `matchRequest` (`MatchRequest`): Match request details.

`callback` (`(requested: boolean) => void`): To check if the match request was successfully sent. | **match_request_exists**: Notify the user that only one match request can be processed at a time.

**match_request_error**: Notify the user that the match request failed to send. | +| **match_cancel_request** | Cancels the match request | `uid` (string): ID of the user. | None | +| **match_accept_request** | Accepts the match request | `uid` (string): ID of the user. | **match_successful**: If both users have accepted the match offer, notify them that the match is successful. | +| **match_decline_request** | Declines the match request | `uid` (string): ID of the user.

`matchId` (string): ID of the user.

`isTimeout` (boolean): Whether the match was declined due to match offer timeout. | **match_unsuccessful**: If the match was not declined due to match offer timeout (was explicitly rejected by the user), notify the partner user that the match is unsuccessful. | +| **rematch_request** | Sends a rematch request | `matchId` (string): ID of the match.

`partnerId` (string): ID of the partner user.

`rematchRequest` (`MatchRequest`): Rematch request details.

`callback` (`(requested: boolean) => void`): To check if the rematch request was successfully sent. | **match_request_error**: Notify the user that the rematch request failed to send. | +| **match_end_request** | User leaves the matching process upon leaving the collaboration session | `uid` (string): ID of the user.

`matchId` (string): ID of the match. | None | + +### Event Parameter Types + +```typescript +interface MatchUser { + id: string; + username: string; + profile?: string; +} + +interface MatchRequest { + user: MatchUser; + complexity: string; + category: string; + language: string; + timeout: number; +} +``` diff --git a/backend/matching-service/docs/images/postman-setup1.png b/backend/matching-service/docs/images/postman-setup1.png new file mode 100644 index 0000000000000000000000000000000000000000..ac1ccb9bde1990492250360cd616210ad2855445 GIT binary patch literal 49363 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 UmoxHW?{B$nY<08pj|Wfx7aE?PC;$Ke literal 0 HcmV?d00001 diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index e06eee07a7..139cc2fa7c 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -15,7 +15,7 @@ import { sendToProducer } from "../config/rabbitmq"; enum MatchEvents { // Receive MATCH_REQUEST = "match_request", - CANCEL_MATCH_REQUEST = "cancel_match_request", + MATCH_CANCEL_REQUEST = "match_cancel_request", MATCH_ACCEPT_REQUEST = "match_accept_request", MATCH_DECLINE_REQUEST = "match_decline_request", REMATCH_REQUEST = "rematch_request", @@ -114,7 +114,7 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { } ); - socket.on(MatchEvents.CANCEL_MATCH_REQUEST, (uid: string) => { + socket.on(MatchEvents.MATCH_CANCEL_REQUEST, (uid: string) => { userConnections.delete(uid); }); @@ -154,7 +154,7 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { matchId: string, partnerId: string, rematchRequest: MatchRequest, - callback: (result: boolean) => void + callback: (requested: boolean) => void ) => { const matchDeleted = handleMatchDelete(matchId); if (matchDeleted) { @@ -233,7 +233,7 @@ const endMatchOnUserDisconnect = (socket: Socket, uid: string) => { if (matchId) { const matchDeleted = handleMatchDelete(matchId); if (matchDeleted) { - socket.to(matchId).emit(MatchEvents.MATCH_UNSUCCESSFUL); // on matching page + socket.to(matchId).emit(MatchEvents.MATCH_UNSUCCESSFUL); } } }; diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 4c0547e9cf..cbf2bc39e6 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -70,6 +70,7 @@ const CollabSessionControls: React.FC = () => { collabSocket.off(CollabEvents.END_SESSION); collabSocket.off(CollabEvents.PARTNER_DISCONNECTED); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -91,6 +92,7 @@ const CollabSessionControls: React.FC = () => { if (qnHistoryId) { setStopTime(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [qnHistoryId]); useEffect(() => { diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 32b0e2dfb9..88fb9bb3aa 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -35,7 +35,7 @@ type MatchCriteria = { enum MatchEvents { // Send MATCH_REQUEST = "match_request", - CANCEL_MATCH_REQUEST = "cancel_match_request", + MATCH_CANCEL_REQUEST = "match_cancel_request", MATCH_ACCEPT_REQUEST = "match_accept_request", MATCH_DECLINE_REQUEST = "match_decline_request", REMATCH_REQUEST = "rematch_request", @@ -365,7 +365,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { appNavigate(MatchPaths.HOME); return; case MatchPaths.MATCHING: - matchSocket.emit(MatchEvents.CANCEL_MATCH_REQUEST, matchUser?.id); + matchSocket.emit(MatchEvents.MATCH_CANCEL_REQUEST, matchUser?.id); appNavigate(MatchPaths.HOME); return; case MatchPaths.MATCHED: @@ -446,7 +446,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const matchingTimeout = () => { - matchSocket.emit(MatchEvents.CANCEL_MATCH_REQUEST, matchUser?.id); + matchSocket.emit(MatchEvents.MATCH_CANCEL_REQUEST, matchUser?.id); appNavigate(MatchPaths.TIMEOUT); }; diff --git a/frontend/src/pages/NewQuestion/index.tsx b/frontend/src/pages/NewQuestion/index.tsx index 31d5a0a54c..0e60d05cd3 100644 --- a/frontend/src/pages/NewQuestion/index.tsx +++ b/frontend/src/pages/NewQuestion/index.tsx @@ -1,3 +1,5 @@ +/* eslint-disable react-refresh/only-export-components */ + import { useEffect, useState, useReducer } from "react"; import { useNavigate } from "react-router-dom"; import { From c147f6c1b516ec7212a9416936c6311831fc9b2e Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 12 Nov 2024 00:31:36 +0800 Subject: [PATCH 167/192] Update collab service readme --- backend/collab-service/README.md | 42 +++++++++++++----- backend/collab-service/docs/image2.png | Bin 21421 -> 0 bytes backend/collab-service/docs/image3.png | Bin 28515 -> 0 bytes backend/collab-service/docs/image4.png | Bin 14934 -> 0 bytes .../{image1.png => images/postman-setup1.png} | Bin .../docs/images/postman-setup2.png | Bin 0 -> 34305 bytes .../docs/images/postman-setup3.png | Bin 0 -> 27173 bytes .../docs/images/postman-setup4.png | Bin 0 -> 24270 bytes backend/collab-service/package-lock.json | 35 --------------- backend/collab-service/package.json | 2 - backend/collab-service/src/app.ts | 14 ------ .../src/middlewares/basicAccessControl.ts | 1 - .../collab-service/src/routes/collabRoutes.ts | 5 --- backend/collab-service/swagger.yml | 23 ---------- backend/communication-service/README.md | 26 +++++------ .../src/middlewares/basicAccessControl.ts | 1 - backend/matching-service/README.md | 28 ++++++------ .../docs/images/postman-setup2.png | Bin 34894 -> 28244 bytes .../docs/images/postman-setup3.png | Bin 29937 -> 27906 bytes .../docs/images/postman-setup4.png | Bin 25847 -> 23607 bytes backend/matching-service/src/app.ts | 1 + .../src/handlers/websocketHandler.ts | 12 ----- frontend/src/contexts/MatchContext.tsx | 1 - 23 files changed, 58 insertions(+), 133 deletions(-) delete mode 100644 backend/collab-service/docs/image2.png delete mode 100644 backend/collab-service/docs/image3.png delete mode 100644 backend/collab-service/docs/image4.png rename backend/collab-service/docs/{image1.png => images/postman-setup1.png} (100%) create mode 100644 backend/collab-service/docs/images/postman-setup2.png create mode 100644 backend/collab-service/docs/images/postman-setup3.png create mode 100644 backend/collab-service/docs/images/postman-setup4.png delete mode 100644 backend/collab-service/src/routes/collabRoutes.ts delete mode 100644 backend/collab-service/swagger.yml diff --git a/backend/collab-service/README.md b/backend/collab-service/README.md index 45cbd1def9..9535df5f4f 100644 --- a/backend/collab-service/README.md +++ b/backend/collab-service/README.md @@ -35,22 +35,40 @@ - You should open 2 tabs on Postman to simulate 2 users in the Collab Service. - Select the `Socket.IO` option and set URL to `http://localhost:3003`. Click `Connect`. - ![image1.png](docs/image1.png) + + ![image1.png](./docs/images/postman-setup1.png) - Add the following events in the `Events` tab and listen to them. - ![image2.png](docs/image2.png) - - To send a message, go to the `Message` tab and ensure that your message is being parsed as `JSON`. - ![image3.png](docs/image3.png) + ![image2.png](./docs/images/postman-setup2.png) + + - In the `Headers` tab, add a valid JWT token in the `Authorization` header. + + ![image3.png](./docs/images/postman-setup3.png) + + - In the `Message` tab, select `JSON` in the bottom left dropdown to ensure that your message is being parsed correctly. In the `Event name` input field, enter the name of the event you would like to send a message to. Click on `Send` to send a message. - - In the `Event name` input, input the correct event name. Click on `Send` to send a message. - ![image4.png](docs/image4.png) + ![image4.png](./docs/images/postman-setup4.png) ## Events Available -| Event Name | Description | Parameters | Response Event | -| -------------- | --------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **join** | Joins a collaboration room. | `roomId` (string): ID of the room. | **room_full:** Notify the user if the room is full (only 2 users allowed).
**connected:** Notify the user if successfully connected.
**new_user_connected:** Notify the other user if a new user joins the room. | -| **change** | Sends updated code to other user. | `roomId` (string): ID of the room.
`code` (string): Updated code content. | **code_change:** Notify the other user with the updated code content. | -| **leave** | Leaves the collaboration room. | `roomId` (string): ID of the room. | **partner_left:** Notify the other user when one leaves the room. | -| **disconnect** | Disconnects from the server. | None | **partner_disconnected:** Notify the other user when one is disconnected. | +| Event Name | Description | Parameters | Response Event | +| ------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **join** | Joins a collaboration room | `uid` (string): ID of the user.

`roomId` (string): ID of the room. | **room_ready:** Notify both users in the room that the room is ready (when exactly 2 users have joined). | +| **init_document** | Initializes the server document for the room | `roomId` (string): ID of the room.

`template` (string): Document template.

`uid1` (string): ID of the first user in the room.

`uid2` (string): ID of the second user in the room.

`language` (string): Programming language selected by both users.

`qnId` (string): ID of the selected question.

`qnTitle` (string): Title of the selected question. | **document_ready:** Notify both users in the room that the server document is ready. | +| **update_request** | Sends a document update | `roomId` (string): ID of the room.

`update` (`Uint8Array`): Document update. | **updateV2:** Sends the updated server document to both users in the room.

**document_not_found:** Notify both users in the room that the server document was not found. | +| **update_cursor_request** | Sends a cursor update | `roomId` (string): ID of the room.

`cursor` (`Cursor`): Cursor details. | **update_cursor:** Notify the partner user of the cursor update. | +| **leave** | Leaves the collaboration room | `uid` (string): ID of the user.

`roomId` (string): ID of the room.

`isPartnerNotified` (boolean): Whether the partner user has been notified that this user has left the room. | **partner_disconnected:** Notify the partner user that this user has disconnected from the collaboration session. | +| **end_session_request** | Sends a request to end the collaboration session | `roomId` (string): ID of the room.

`sessionDuration` (number): Duration of the collaboration session. | **end_session:** Notify both users in the room that the collaboration session has ended. | +| **reconnect_request** | Reconnects to the collaboration session | `roomId` (string): ID of the room. | None | + +### Event Parameter Types + +```typescript +interface Cursor { + uid: string; + username: string; + from: number; + to: number; +} +``` diff --git a/backend/collab-service/docs/image2.png b/backend/collab-service/docs/image2.png deleted file mode 100644 index 0c6924d98d03134ba1b4e297be95aba2f09494a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21421 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 MhUZRI8aPG$1xgs_kpKVy diff --git a/backend/collab-service/docs/image3.png b/backend/collab-service/docs/image3.png deleted file mode 100644 index 1a80ceeec92fafb9bf8b01dbb62f7fae4946e317..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28515 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 KXsNKC&;J51`ORAZ literal 0 HcmV?d00001 diff --git a/backend/collab-service/docs/images/postman-setup3.png b/backend/collab-service/docs/images/postman-setup3.png new file mode 100644 index 0000000000000000000000000000000000000000..8abf168e8375609a5d6d76f1a13ffdb805211eab GIT binary patch literal 27173 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 Ls`v*mWqS0D)3UP!Nz_+LT2BDxF9tfe+8rBji< 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 Dfaf;k literal 0 HcmV?d00001 diff --git a/backend/collab-service/package-lock.json b/backend/collab-service/package-lock.json index 009a4e8cca..5feb5553c2 100644 --- a/backend/collab-service/package-lock.json +++ b/backend/collab-service/package-lock.json @@ -18,9 +18,7 @@ "express": "^4.21.1", "redis": "^4.7.0", "socket.io": "^4.8.1", - "swagger-ui-express": "^5.0.1", "y-protocols": "^1.0.6", - "yaml": "^2.6.0", "yjs": "^13.6.20" }, "devDependencies": { @@ -6767,27 +6765,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swagger-ui-dist": { - "version": "5.17.14", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", - "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", - "license": "Apache-2.0" - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "license": "MIT", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -7292,18 +7269,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", - "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index 5529171a82..5937ec1bd2 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -23,9 +23,7 @@ "express": "^4.21.1", "redis": "^4.7.0", "socket.io": "^4.8.1", - "swagger-ui-express": "^5.0.1", "y-protocols": "^1.0.6", - "yaml": "^2.6.0", "yjs": "^13.6.20" }, "devDependencies": { diff --git a/backend/collab-service/src/app.ts b/backend/collab-service/src/app.ts index 9aaff3af65..4bcc963b59 100644 --- a/backend/collab-service/src/app.ts +++ b/backend/collab-service/src/app.ts @@ -1,33 +1,19 @@ import express, { Request, Response } from "express"; import dotenv from "dotenv"; -import fs from "fs"; -import yaml from "yaml"; -import swaggerUi from "swagger-ui-express"; import cors from "cors"; -import collabRoutes from "./routes/collabRoutes.ts"; - dotenv.config(); export const allowedOrigins = process.env.ORIGINS ? process.env.ORIGINS.split(",") : ["http://localhost:5173", "http://127.0.0.1:5173"]; -const file = fs.readFileSync("./swagger.yml", "utf-8"); -const swaggerDocument = yaml.parse(file); - const app = express(); app.use(cors({ origin: allowedOrigins, credentials: true })); app.options("*", cors({ origin: allowedOrigins, credentials: true })); -app.use(express.json()); - -app.use("/api/collab", collabRoutes); - -app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); - app.get("/", (req: Request, res: Response) => { res.status(200).json({ message: "Hello world from collab service" }); }); diff --git a/backend/collab-service/src/middlewares/basicAccessControl.ts b/backend/collab-service/src/middlewares/basicAccessControl.ts index 727ee6783e..5f8c45ceaa 100644 --- a/backend/collab-service/src/middlewares/basicAccessControl.ts +++ b/backend/collab-service/src/middlewares/basicAccessControl.ts @@ -9,7 +9,6 @@ export const verifyUserToken = ( socket.handshake.headers.authorization || socket.handshake.auth.token; verifyToken(token) .then(() => { - console.log("Valid credentials"); next(); }) .catch((err) => { diff --git a/backend/collab-service/src/routes/collabRoutes.ts b/backend/collab-service/src/routes/collabRoutes.ts deleted file mode 100644 index 9da7196e6e..0000000000 --- a/backend/collab-service/src/routes/collabRoutes.ts +++ /dev/null @@ -1,5 +0,0 @@ -import express from "express"; - -const router = express.Router(); - -export default router; diff --git a/backend/collab-service/swagger.yml b/backend/collab-service/swagger.yml deleted file mode 100644 index 5f8bc152eb..0000000000 --- a/backend/collab-service/swagger.yml +++ /dev/null @@ -1,23 +0,0 @@ -openapi: 3.0.0 - -info: - title: Collab Service - version: 1.0.0 - -paths: - /: - get: - tags: - - root - summary: Root - description: Ping the server - responses: - 200: - description: Successful Response - content: - application/json: - schema: - type: object - properties: - message: - type: string diff --git a/backend/communication-service/README.md b/backend/communication-service/README.md index f1e24f9098..cecc56f365 100644 --- a/backend/communication-service/README.md +++ b/backend/communication-service/README.md @@ -22,31 +22,31 @@ ## After Running -1. Using applications like Postman, you can interact with the Communication Service on port 3005. If you wish to change this, please update the `.env` file. +1. Using applications like Postman, you can interact with the Communication Service on port 3005. If you wish to change this, please update the `.env` file. -2. Setting up Socket.IO connection on Postman: +2. Setting up Socket.IO connection on Postman: - - You should open 2 tabs on Postman to simulate 2 users in the Communication Service. + - You should open 2 tabs on Postman to simulate 2 users in the Communication Service. - - Select the `Socket.IO` option and set URL to `http://localhost:3005`. Click `Connect`. + - Select the `Socket.IO` option and set URL to `http://localhost:3005`. Click `Connect`. - ![image1.png](./docs/images/postman-setup1.png) + ![image1.png](./docs/images/postman-setup1.png) - - Add the following events in the `Events` tab and listen to them. + - Add the following events in the `Events` tab and listen to them. - ![image2.png](./docs/images/postman-setup2.png) + ![image2.png](./docs/images/postman-setup2.png) - - Add a valid JWT token in the `Authorization` header. + - Add a valid JWT token in the `Authorization` header. - ![image3.png](./docs/images/postman-setup3.png) + ![image3.png](./docs/images/postman-setup3.png) - - In the `Event name` input, input the correct event name. Click on `Send` to send a message. + - In the `Event name` input, input the correct event name. Click on `Send` to send a message. - ![image4.png](./docs/images/postman-setup4.png) + ![image4.png](./docs/images/postman-setup4.png) - - To send a message, go to the `Message` tab and ensure that your message is being parsed as `JSON`. + - To send a message, go to the `Message` tab and ensure that your message is being parsed as `JSON`. - ![image5.png](./docs/images/postman-setup5.png) + ![image5.png](./docs/images/postman-setup5.png) ## Events Available diff --git a/backend/communication-service/src/middlewares/basicAccessControl.ts b/backend/communication-service/src/middlewares/basicAccessControl.ts index 15088e9a86..a58e7314c9 100644 --- a/backend/communication-service/src/middlewares/basicAccessControl.ts +++ b/backend/communication-service/src/middlewares/basicAccessControl.ts @@ -9,7 +9,6 @@ export const verifyUserToken = ( socket.handshake.headers.authorization || socket.handshake.auth.token; verifyToken(token) .then(() => { - console.log("Valid credentials"); next(); }) .catch((err) => { diff --git a/backend/matching-service/README.md b/backend/matching-service/README.md index ea095a5565..9b56c363f5 100644 --- a/backend/matching-service/README.md +++ b/backend/matching-service/README.md @@ -36,32 +36,32 @@ - Select the `Socket.IO` option and set URL to `http://localhost:3002`. Click `Connect`. - ![image1.png](./docs/images/postman-setup1.png) + ![image1.png](./docs/images/postman-setup1.png) - Add the following events in the `Events` tab and listen to them. - ![image2.png](./docs/images/postman-setup2.png) + ![image2.png](./docs/images/postman-setup2.png) - In the `Headers` tab, add a valid JWT token in the `Authorization` header. - ![image3.png](./docs/images/postman-setup3.png) + ![image3.png](./docs/images/postman-setup3.png) - In the `Message` tab, select `JSON` in the bottom left dropdown to ensure that your message is being parsed correctly. In the `Event name` input field, enter the name of the event you would like to send a message to. Click on `Send` to send a message. - ![image4.png](./docs/images/postman-setup4.png) + ![image4.png](./docs/images/postman-setup4.png) ## Events Available -| Event Name | Description | Parameters | Response Event | -| ------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **user_connected** | User joins the matching process | `uid` (string): ID of the user. | None | -| **user_disconnected** | User leaves the matching process | `uid` (string): ID of the user. | **match_unsuccessful**: If the user left during the match offer phase, notify the partner user that the match was unsuccessful | -| **match_request** | Sends a match request | `matchRequest` (`MatchRequest`): Match request details.

`callback` (`(requested: boolean) => void`): To check if the match request was successfully sent. | **match_request_exists**: Notify the user that only one match request can be processed at a time.

**match_request_error**: Notify the user that the match request failed to send. | -| **match_cancel_request** | Cancels the match request | `uid` (string): ID of the user. | None | -| **match_accept_request** | Accepts the match request | `uid` (string): ID of the user. | **match_successful**: If both users have accepted the match offer, notify them that the match is successful. | -| **match_decline_request** | Declines the match request | `uid` (string): ID of the user.

`matchId` (string): ID of the user.

`isTimeout` (boolean): Whether the match was declined due to match offer timeout. | **match_unsuccessful**: If the match was not declined due to match offer timeout (was explicitly rejected by the user), notify the partner user that the match is unsuccessful. | -| **rematch_request** | Sends a rematch request | `matchId` (string): ID of the match.

`partnerId` (string): ID of the partner user.

`rematchRequest` (`MatchRequest`): Rematch request details.

`callback` (`(requested: boolean) => void`): To check if the rematch request was successfully sent. | **match_request_error**: Notify the user that the rematch request failed to send. | -| **match_end_request** | User leaves the matching process upon leaving the collaboration session | `uid` (string): ID of the user.

`matchId` (string): ID of the match. | None | +| Event Name | Description | Parameters | Response Event | +| ------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **user_connected** | User joins the matching process | `uid` (string): ID of the user. | None | +| **user_disconnected** | User leaves the matching process | `uid` (string): ID of the user. | **match_unsuccessful**: If the user left during the match offer phase, notify the partner user that the match was unsuccessful | +| **match_request** | Sends a match request | `matchRequest` (`MatchRequest`): Match request details.

`callback` (`(requested: boolean) => void`): To check if the match request was successfully sent. | **match_found**: Notify the user that a match has been found.

**match_request_exists**: Notify the user that only one match request can be processed at a time.

**match_request_error**: Notify the user that the match request failed to send. | +| **match_cancel_request** | Cancels the match request | `uid` (string): ID of the user. | None | +| **match_accept_request** | Accepts the match request | `uid` (string): ID of the user. | **match_successful**: If both users have accepted the match offer, notify them that the match is successful. | +| **match_decline_request** | Declines the match request | `uid` (string): ID of the user.

`matchId` (string): ID of the user.

`isTimeout` (boolean): Whether the match was declined due to match offer timeout. | **match_unsuccessful**: If the match was not declined due to match offer timeout (was explicitly rejected by the user), notify the partner user that the match is unsuccessful. | +| **rematch_request** | Sends a rematch request | `matchId` (string): ID of the match.

`partnerId` (string): ID of the partner user.

`rematchRequest` (`MatchRequest`): Rematch request details.

`callback` (`(requested: boolean) => void`): To check if the rematch request was successfully sent. | **match_request_error**: Notify the user that the rematch request failed to send. | +| **match_end_request** | User leaves the matching process upon leaving the collaboration session | `uid` (string): ID of the user.

`matchId` (string): ID of the match. | None | ### Event Parameter Types diff --git a/backend/matching-service/docs/images/postman-setup2.png b/backend/matching-service/docs/images/postman-setup2.png index 3280d1111ceb5d07557caf6b3a7f43042746bb86..b2887f79b5cee38058acea8e6c911b021cbcce14 100644 GIT binary patch literal 28244 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 z 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 KC-RP42mcq{JH%uF literal 34894 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 UmoxHW?{B$nY<08pj|Wfx7aE?PC;$Ke diff --git a/backend/matching-service/src/app.ts b/backend/matching-service/src/app.ts index 309e97485d..ac823e88af 100644 --- a/backend/matching-service/src/app.ts +++ b/backend/matching-service/src/app.ts @@ -11,6 +11,7 @@ export const allowedOrigins = process.env.ORIGINS const app = express(); app.use(cors({ origin: allowedOrigins, credentials: true })); + app.options("*", cors({ origin: allowedOrigins, credentials: true })); app.get("/", (req: Request, res: Response) => { diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index 139cc2fa7c..cb4f49be72 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -20,7 +20,6 @@ enum MatchEvents { MATCH_DECLINE_REQUEST = "match_decline_request", REMATCH_REQUEST = "rematch_request", MATCH_END_REQUEST = "match_end_request", - MATCH_STATUS_REQUEST = "match_status_request", USER_CONNECTED = "user_connected", USER_DISCONNECTED = "user_disconnected", @@ -182,17 +181,6 @@ export const handleWebsocketMatchEvents = (socket: Socket) => { handleMatchDelete(matchId); }); - socket.on( - MatchEvents.MATCH_STATUS_REQUEST, - ( - uid: string, - callback: (match: { matchId: string; partner: MatchUser } | null) => void - ) => { - const match = getMatchByUid(uid); - callback(match); - } - ); - socket.on(MatchEvents.SOCKET_DISCONNECT, () => { for (const [uid, userConnection] of userConnections) { if (userConnection.socket.id === socket.id) { diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 88fb9bb3aa..49a43d7047 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -40,7 +40,6 @@ enum MatchEvents { MATCH_DECLINE_REQUEST = "match_decline_request", REMATCH_REQUEST = "rematch_request", MATCH_END_REQUEST = "match_end_request", - MATCH_STATUS_REQUEST = "match_status_request", USER_CONNECTED = "user_connected", USER_DISCONNECTED = "user_disconnected", From db94a595e6d05c6f2ff209a3f98bbdad79f31101 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 12 Nov 2024 10:53:04 +0800 Subject: [PATCH 168/192] Update matching service readme --- backend/matching-service/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/matching-service/README.md b/backend/matching-service/README.md index 9b56c363f5..5d42960986 100644 --- a/backend/matching-service/README.md +++ b/backend/matching-service/README.md @@ -55,12 +55,12 @@ | Event Name | Description | Parameters | Response Event | | ------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **user_connected** | User joins the matching process | `uid` (string): ID of the user. | None | -| **user_disconnected** | User leaves the matching process | `uid` (string): ID of the user. | **match_unsuccessful**: If the user left during the match offer phase, notify the partner user that the match was unsuccessful | +| **user_disconnected** | User leaves the matching process (if not reconnected within 3 seconds) | `uid` (string): ID of the user. | **match_unsuccessful**: If the user left during the match offer phase, notify the partner user that the match was unsuccessful. | | **match_request** | Sends a match request | `matchRequest` (`MatchRequest`): Match request details.

`callback` (`(requested: boolean) => void`): To check if the match request was successfully sent. | **match_found**: Notify the user that a match has been found.

**match_request_exists**: Notify the user that only one match request can be processed at a time.

**match_request_error**: Notify the user that the match request failed to send. | -| **match_cancel_request** | Cancels the match request | `uid` (string): ID of the user. | None | -| **match_accept_request** | Accepts the match request | `uid` (string): ID of the user. | **match_successful**: If both users have accepted the match offer, notify them that the match is successful. | -| **match_decline_request** | Declines the match request | `uid` (string): ID of the user.

`matchId` (string): ID of the user.

`isTimeout` (boolean): Whether the match was declined due to match offer timeout. | **match_unsuccessful**: If the match was not declined due to match offer timeout (was explicitly rejected by the user), notify the partner user that the match is unsuccessful. | -| **rematch_request** | Sends a rematch request | `matchId` (string): ID of the match.

`partnerId` (string): ID of the partner user.

`rematchRequest` (`MatchRequest`): Rematch request details.

`callback` (`(requested: boolean) => void`): To check if the rematch request was successfully sent. | **match_request_error**: Notify the user that the rematch request failed to send. | +| **match_cancel_request** | Cancels the match request before a match has been found | `uid` (string): ID of the user. | None | +| **match_accept_request** | Accepts the match offer | `uid` (string): ID of the user. | **match_successful**: If both users have accepted the match offer, notify them that the match is successful. | +| **match_decline_request** | Declines the match offer | `uid` (string): ID of the user.

`matchId` (string): ID of the user.

`isTimeout` (boolean): Whether the match was declined due to match offer timeout. | **match_unsuccessful**: If the match was not declined due to match offer timeout (was explicitly rejected by the user), notify the partner user that the match is unsuccessful. | +| **rematch_request** | Sends a rematch request | `matchId` (string): ID of the match.

`partnerId` (string): ID of the partner user.

`rematchRequest` (`MatchRequest`): Rematch request details.

`callback` (`(requested: boolean) => void`): To check if the rematch request was successfully sent. | **match_unsuccessful**: Notify the partner user that the match is unsuccessful.

**match_request_error**: Notify the user that the rematch request failed to send. | | **match_end_request** | User leaves the matching process upon leaving the collaboration session | `uid` (string): ID of the user.

`matchId` (string): ID of the match. | None | ### Event Parameter Types From e98e697db1a37ab2d57b6cdb2d80be333f6ff187 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 12 Nov 2024 12:08:04 +0800 Subject: [PATCH 169/192] Add tests for matching socket --- backend/matching-service/package-lock.json | 105 +++++++++++++++-- backend/matching-service/package.json | 2 + .../src/handlers/websocketHandler.ts | 2 +- .../tests/webSocketHandler.spec.ts | 107 +++++++++++++++++- 4 files changed, 205 insertions(+), 11 deletions(-) diff --git a/backend/matching-service/package-lock.json b/backend/matching-service/package-lock.json index ce7092cf4d..fe30e9c82e 100644 --- a/backend/matching-service/package-lock.json +++ b/backend/matching-service/package-lock.json @@ -27,6 +27,7 @@ "@types/jest": "^29.5.13", "@types/node": "^22.7.5", "@types/socket.io": "^3.0.2", + "@types/socket.io-client": "^1.4.36", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^10.0.0", @@ -34,6 +35,7 @@ "eslint": "^9.12.0", "globals": "^15.11.0", "jest": "^29.7.0", + "socket.io-client": "^4.8.1", "supertest": "^7.0.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", @@ -2137,6 +2139,12 @@ "socket.io": "*" } }, + "node_modules/@types/socket.io-client": { + "version": "1.4.36", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", + "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3386,16 +3394,16 @@ } }, "node_modules/engine.io": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.1.tgz", - "integrity": "sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -3405,6 +3413,42 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", + "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": 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", @@ -3414,9 +3458,9 @@ } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "engines": { "node": ">= 0.6" } @@ -6294,6 +6338,44 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": 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", @@ -7067,6 +7149,15 @@ } } }, + "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==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/matching-service/package.json b/backend/matching-service/package.json index 7b681d8bfe..3731525c6f 100644 --- a/backend/matching-service/package.json +++ b/backend/matching-service/package.json @@ -31,6 +31,7 @@ "@types/jest": "^29.5.13", "@types/node": "^22.7.5", "@types/socket.io": "^3.0.2", + "@types/socket.io-client": "^1.4.36", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.6", "@types/uuid": "^10.0.0", @@ -38,6 +39,7 @@ "eslint": "^9.12.0", "globals": "^15.11.0", "jest": "^29.7.0", + "socket.io-client": "^4.8.1", "supertest": "^7.0.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index 92f9861836..61f8baea43 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -13,7 +13,7 @@ import { io } from "../server"; import { v4 as uuidv4 } from "uuid"; import { getRandomQuestion } from "../api/questionService"; -enum MatchEvents { +export enum MatchEvents { // Receive MATCH_REQUEST = "match_request", CANCEL_MATCH_REQUEST = "cancel_match_request", diff --git a/backend/matching-service/tests/webSocketHandler.spec.ts b/backend/matching-service/tests/webSocketHandler.spec.ts index 1b286ac176..a2833108da 100644 --- a/backend/matching-service/tests/webSocketHandler.spec.ts +++ b/backend/matching-service/tests/webSocketHandler.spec.ts @@ -1,5 +1,106 @@ -describe("Test web socket", () => { - it("Test", () => { - expect(true); +import { createServer } from "node:http"; +import { type AddressInfo } from "node:net"; +import ioc from "socket.io-client"; +import { Server, Socket } from "socket.io"; +import { MatchEvents } from "../src/handlers/websocketHandler"; +import { MatchUser } from "../src/handlers/matchHandler"; + +describe("Matching service web socket", () => { + let io: Server, serverSocket: Socket, clientSocket: SocketIOClient.Socket; + + beforeAll((done) => { + const httpServer = createServer(); + io = new Server(httpServer); + httpServer.listen(() => { + const port = (httpServer.address() as AddressInfo).port; + clientSocket = ioc(`http://localhost:${port}`); + io.on("connection", (socket) => { + serverSocket = socket; + }); + clientSocket.on("connect", done); + }); + }); + + afterAll(() => { + io.close(); + clientSocket.close(); + }); + + it("Match found", (done) => { + const matchIdSent = "123"; + const user1Sent = { + id: "456", + username: "user1", + }; + const user2Sent = { + id: "789", + username: "user2", + }; + clientSocket.on( + MatchEvents.MATCH_FOUND, + ({ + matchId, + user1, + user2, + }: { + matchId: string; + user1: MatchUser; + user2: MatchUser; + }) => { + expect(matchId).toEqual(matchIdSent); + expect(user1).toEqual(user1Sent); + expect(user2).toEqual(user2Sent); + done(); + } + ); + serverSocket.emit(MatchEvents.MATCH_FOUND, { + matchId: matchIdSent, + user1: user1Sent, + user2: user2Sent, + }); + }); + + it("Match successful", (done) => { + const questionIdSent = "123"; + const questionTitleSent = "456"; + clientSocket.on( + MatchEvents.MATCH_SUCCESSFUL, + ({ + questionId, + questionTitle, + }: { + questionId: string; + questionTitle: string; + }) => { + expect(questionId).toEqual(questionIdSent); + expect(questionTitle).toEqual(questionTitleSent); + done(); + } + ); + serverSocket.emit(MatchEvents.MATCH_SUCCESSFUL, { + questionId: questionIdSent, + questionTitle: questionTitleSent, + }); + }); + + it("Match unsuccessful", (done) => { + clientSocket.on(MatchEvents.MATCH_UNSUCCESSFUL, () => { + done(); + }); + serverSocket.emit(MatchEvents.MATCH_UNSUCCESSFUL); + }); + + it("Match request exists", (done) => { + clientSocket.on(MatchEvents.MATCH_REQUEST_EXISTS, () => { + done(); + }); + serverSocket.emit(MatchEvents.MATCH_REQUEST_EXISTS); + }); + + it("Match request error", (done) => { + clientSocket.on(MatchEvents.MATCH_REQUEST_ERROR, () => { + done(); + }); + serverSocket.emit(MatchEvents.MATCH_REQUEST_ERROR); }); }); From 20ce8bbc098432fbc57797c94d147a35bec6467d Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 12 Nov 2024 12:53:47 +0800 Subject: [PATCH 170/192] Add tests for collab socket --- backend/collab-service/package-lock.json | 102 ++++++++++++++ backend/collab-service/package.json | 3 + .../src/handlers/websocketHandler.ts | 2 +- .../tests/webSocketHandler.spec.ts | 131 +++++++++++++++++- 4 files changed, 234 insertions(+), 4 deletions(-) diff --git a/backend/collab-service/package-lock.json b/backend/collab-service/package-lock.json index 009a4e8cca..9caa0df8f3 100644 --- a/backend/collab-service/package-lock.json +++ b/backend/collab-service/package-lock.json @@ -29,11 +29,14 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.9", + "@types/socket.io": "^3.0.2", + "@types/socket.io-client": "^1.4.36", "@types/swagger-ui-express": "^4.1.6", "cross-env": "^7.0.3", "eslint": "^9.13.0", "globals": "^15.11.0", "jest": "^29.7.0", + "socket.io-client": "^4.8.1", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsx": "^4.19.1", @@ -2165,6 +2168,22 @@ "@types/send": "*" } }, + "node_modules/@types/socket.io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz", + "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==", + "deprecated": "This is a stub types definition. socket.io provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "socket.io": "*" + } + }, + "node_modules/@types/socket.io-client": { + "version": "1.4.36", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", + "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3452,6 +3471,42 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", + "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": 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", @@ -6562,6 +6617,44 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": 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", @@ -7256,6 +7349,15 @@ } } }, + "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==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y-protocols": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index 5529171a82..ed644d4bfc 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -34,11 +34,14 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.7.9", + "@types/socket.io": "^3.0.2", + "@types/socket.io-client": "^1.4.36", "@types/swagger-ui-express": "^4.1.6", "cross-env": "^7.0.3", "eslint": "^9.13.0", "globals": "^15.11.0", "jest": "^29.7.0", + "socket.io-client": "^4.8.1", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "tsx": "^4.19.1", diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 3a049e2d18..efa26c239d 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -4,7 +4,7 @@ import redisClient from "../config/redis"; import { Doc, applyUpdateV2, encodeStateAsUpdateV2 } from "yjs"; import { createQuestionHistory } from "../api/questionHistoryService"; -enum CollabEvents { +export enum CollabEvents { // Receive JOIN = "join", LEAVE = "leave", diff --git a/backend/collab-service/tests/webSocketHandler.spec.ts b/backend/collab-service/tests/webSocketHandler.spec.ts index 1b286ac176..35e3ae7447 100644 --- a/backend/collab-service/tests/webSocketHandler.spec.ts +++ b/backend/collab-service/tests/webSocketHandler.spec.ts @@ -1,5 +1,130 @@ -describe("Test web socket", () => { - it("Test", () => { - expect(true); +import { createServer } from "node:http"; +import { type AddressInfo } from "node:net"; +import ioc from "socket.io-client"; +import { Server, Socket } from "socket.io"; +import { CollabEvents } from "../src/handlers/websocketHandler"; + +describe("Collab service web socket", () => { + let io: Server, serverSocket: Socket, clientSocket: SocketIOClient.Socket; + + beforeAll((done) => { + const httpServer = createServer(); + io = new Server(httpServer); + httpServer.listen(() => { + const port = (httpServer.address() as AddressInfo).port; + clientSocket = ioc(`http://localhost:${port}`); + io.on("connection", (socket) => { + serverSocket = socket; + }); + clientSocket.on("connect", done); + }); + }); + + afterAll(() => { + io.close(); + clientSocket.close(); + }); + + it("Room ready", (done) => { + const isRoomReady = true; + clientSocket.on( + CollabEvents.ROOM_READY, + ({ ready }: { ready: boolean }) => { + expect(ready).toEqual(isRoomReady); + done(); + } + ); + serverSocket.emit(CollabEvents.ROOM_READY, { + ready: isRoomReady, + }); + }); + + it("Document ready", (done) => { + const questionHistoryIdSent = "123"; + clientSocket.on( + CollabEvents.DOCUMENT_READY, + ({ questionHistoryId }: { questionHistoryId: string }) => { + expect(questionHistoryId).toEqual(questionHistoryIdSent); + done(); + } + ); + serverSocket.emit(CollabEvents.DOCUMENT_READY, { + questionHistoryId: questionHistoryIdSent, + }); + }); + + it("Document not found", (done) => { + clientSocket.on(CollabEvents.DOCUMENT_NOT_FOUND, () => { + done(); + }); + serverSocket.emit(CollabEvents.DOCUMENT_NOT_FOUND); + }); + + it("Document update", (done) => { + const updateSent = new Uint8Array([1, 2, 3]); + clientSocket.on( + CollabEvents.UPDATE, + ({ update }: { update: Uint8Array }) => { + expect(Array.from(update)).toEqual(Array.from(updateSent)); + done(); + } + ); + serverSocket.emit(CollabEvents.UPDATE, { + update: updateSent, + }); + }); + + it("Cursor update", (done) => { + const uidSent = "123"; + const usernameSent = "user"; + const fromSent = 1; + const toSent = 5; + clientSocket.on( + CollabEvents.UPDATE_CURSOR, + ({ + uid, + username, + from, + to, + }: { + uid: string; + username: string; + from: number; + to: number; + }) => { + expect(uid).toEqual(uidSent); + expect(username).toEqual(usernameSent); + expect(from).toEqual(fromSent); + expect(to).toEqual(toSent); + done(); + } + ); + serverSocket.emit(CollabEvents.UPDATE_CURSOR, { + uid: uidSent, + username: usernameSent, + from: fromSent, + to: toSent, + }); + }); + + it("End session", (done) => { + const sessionDurationSent = 30; + clientSocket.on( + CollabEvents.END_SESSION, + ({ sessionDuration }: { sessionDuration: number }) => { + expect(sessionDuration).toEqual(sessionDurationSent); + done(); + } + ); + serverSocket.emit(CollabEvents.END_SESSION, { + sessionDuration: sessionDurationSent, + }); + }); + + it("Partner disconnected", (done) => { + clientSocket.on(CollabEvents.PARTNER_DISCONNECTED, () => { + done(); + }); + serverSocket.emit(CollabEvents.PARTNER_DISCONNECTED); }); }); From 453f25f35246e6614b7fe5702b93e3c91df9335e Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 12 Nov 2024 13:32:28 +0800 Subject: [PATCH 171/192] Remove commented code --- .../src/handlers/matchHandler.ts | 13 --------- .../src/handlers/websocketHandler.ts | 1 - backend/question-service/src/app.ts | 19 ------------- backend/user-service/src/app.ts | 19 ------------- backend/user-service/tests/authRoutes.spec.ts | 8 ------ frontend/src/components/Chat/index.tsx | 1 - frontend/src/components/Layout/index.tsx | 1 - .../src/components/Navbar/Navbar.test.tsx | 4 ++- .../QuestionCategoryAutoComplete.test.tsx | 28 ------------------- .../QuestionCategoryAutoComplete/index.tsx | 18 ------------ frontend/src/pages/Home/index.tsx | 14 +++++----- frontend/src/pages/Matching/index.tsx | 4 +-- frontend/src/reducers/questionReducer.ts | 2 -- frontend/src/utils/constants.ts | 5 ++-- 14 files changed, 14 insertions(+), 123 deletions(-) diff --git a/backend/matching-service/src/handlers/matchHandler.ts b/backend/matching-service/src/handlers/matchHandler.ts index 54beebfaa1..6517fd64c6 100644 --- a/backend/matching-service/src/handlers/matchHandler.ts +++ b/backend/matching-service/src/handlers/matchHandler.ts @@ -80,19 +80,6 @@ export const getMatchIdByUid = (uid: string): string | null => { return null; }; -export const getMatchByUid = ( - uid: string -): { matchId: string; partner: MatchUser } | null => { - for (const [matchId, match] of matches) { - if (match.matchUser1.id === uid) { - return { matchId: matchId, partner: match.matchUser2 }; - } else if (match.matchUser2.id === uid) { - return { matchId: matchId, partner: match.matchUser1 }; - } - } - return null; -}; - export const getMatchById = (matchId: string): Match | undefined => { return matches.get(matchId); }; diff --git a/backend/matching-service/src/handlers/websocketHandler.ts b/backend/matching-service/src/handlers/websocketHandler.ts index f561f976e1..565d0682a7 100644 --- a/backend/matching-service/src/handlers/websocketHandler.ts +++ b/backend/matching-service/src/handlers/websocketHandler.ts @@ -3,7 +3,6 @@ import { handleMatchAccept, handleMatchDelete, getMatchIdByUid, - getMatchByUid, getMatchById, } from "./matchHandler"; import { io } from "../server"; diff --git a/backend/question-service/src/app.ts b/backend/question-service/src/app.ts index 86066cbe45..72439683fb 100644 --- a/backend/question-service/src/app.ts +++ b/backend/question-service/src/app.ts @@ -21,25 +21,6 @@ const app = express(); app.use(cors({ origin: allowedOrigins, credentials: true })); app.options("*", cors({ origin: allowedOrigins, credentials: true })); -// To handle CORS Errors -// app.use((req: Request, res: Response, next: NextFunction) => { -// res.header("Access-Control-Allow-Origin", req.headers.origin); // "*" -> Allow all links to access - -// res.header( -// "Access-Control-Allow-Headers", -// "Origin, X-Requested-With, Content-Type, Accept, Authorization", -// ); - -// // Browsers usually send this before PUT or POST Requests -// if (req.method === "OPTIONS") { -// res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH"); -// return res.status(200).json({}); -// } - -// // Continue Route Processing -// next(); -// }); - app.use(express.json()); app.use("/api/questions", questionRoutes); app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); diff --git a/backend/user-service/src/app.ts b/backend/user-service/src/app.ts index 346e466d8c..4bcda2b6bc 100644 --- a/backend/user-service/src/app.ts +++ b/backend/user-service/src/app.ts @@ -22,25 +22,6 @@ app.use(express.json()); app.use(cors({ origin: allowedOrigins, credentials: true })); // config cors so that front-end can use app.options("*", cors({ origin: allowedOrigins, credentials: true })); -// To handle CORS Errors -// app.use((req: Request, res: Response, next: NextFunction) => { -// res.header("Access-Control-Allow-Origin", req.headers.origin); // "*" -> Allow all links to access - -// res.header( -// "Access-Control-Allow-Headers", -// "Origin, X-Requested-With, Content-Type, Accept, Authorization" -// ); - -// // Browsers usually send this before PUT or POST Requests -// if (req.method === "OPTIONS") { -// res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH"); -// return res.status(200).json({}); -// } - -// // Continue Route Processing -// next(); -// }); - app.use("/api/users", userRoutes); app.use("/api/auth", authRoutes); app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); diff --git a/backend/user-service/tests/authRoutes.spec.ts b/backend/user-service/tests/authRoutes.spec.ts index 044d45f900..5ab7443208 100644 --- a/backend/user-service/tests/authRoutes.spec.ts +++ b/backend/user-service/tests/authRoutes.spec.ts @@ -101,10 +101,6 @@ describe("Auth routes", () => { expect(res.status).toBe(401); }); - it("Verify token but users not found", async () => { - // TODO - }); - it("Verify token", async () => { const { email, password } = await insertNonAdminUser(); @@ -164,8 +160,4 @@ describe("Auth routes", () => { expect(res.status).toBe(403); }); - - it("Verify if user is owner or admin", async () => { - // TODO - }); }); diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 7bea1c2770..01554516fd 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -60,7 +60,6 @@ const Chat: React.FC = ({ isActive }) => { return () => { communicationSocket.emit(CommunicationEvents.USER_DISCONNECT); - // setMessages([]); // clear the earlier messages in dev mode }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/frontend/src/components/Layout/index.tsx b/frontend/src/components/Layout/index.tsx index d4e932ad5b..ee75bd242d 100644 --- a/frontend/src/components/Layout/index.tsx +++ b/frontend/src/components/Layout/index.tsx @@ -10,7 +10,6 @@ const Layout: React.FC = () => { flexDirection: "column", minHeight: "100vh", minWidth: "755px", - // minInlineSize: "100vw", }} > diff --git a/frontend/src/components/Navbar/Navbar.test.tsx b/frontend/src/components/Navbar/Navbar.test.tsx index 42a7dccb93..1644d6e660 100644 --- a/frontend/src/components/Navbar/Navbar.test.tsx +++ b/frontend/src/components/Navbar/Navbar.test.tsx @@ -26,12 +26,14 @@ beforeEach(() => { retryMatch: jest.fn(), matchingTimeout: jest.fn(), matchOfferTimeout: jest.fn(), - verifyMatchStatus: jest.fn(), + getMatchId: jest.fn(), matchUser: null, matchCriteria: null, partner: null, matchPending: false, loading: false, + questionId: null, + questionTitle: null, })); }); diff --git a/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx b/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx index 5579d7e9eb..abbad69c7f 100644 --- a/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx +++ b/frontend/src/components/QuestionCategoryAutoComplete/QuestionCategoryAutoComplete.test.tsx @@ -38,34 +38,6 @@ describe("Question Category Auto Complete", () => { await waitFor(() => expect(screen.getByText("DFS")).toBeInTheDocument()); }); - // it("Adding a new category not from the category list", async () => { - // const { rerender } = render( - // - // ); - - // const input = screen.getByLabelText("Category"); - // fireEvent.change(input, { target: { value: "New Category" } }); - - // const valueAdded = 'Add: "New Category"'; - // expect(await screen.findByText(valueAdded)).toBeInTheDocument(); - - // fireEvent.click(screen.getByText(valueAdded)); - - // const updatedCategories = [...selectedCategories, "New Category"]; - - // rerender( - // - // ); - - // expect(screen.getByText("New Category")).toBeInTheDocument(); - // }); - it("Remove a category from selected categories", async () => { render( { - // const newValue = - // newCategoriesSelected[newCategoriesSelected.length - 1]; - // if (typeof newValue === "string" && newValue.startsWith(`Add: "`)) { - // const newCategory = newValue.slice(6, -1); - // state.questionCategories.push(newCategory); - // setSelectedCategories((prev) => [...prev, newCategory]); - // } else { - // setSelectedCategories(newCategoriesSelected); - // } setSelectedCategories(newCategoriesSelected); }} filterOptions={(options, params) => { const filtered = filter(options, params); - - // const { inputValue } = params; - - // const isExisting = options.some((option) => inputValue === option); - - // if (inputValue !== "" && !isExisting) { - // filtered.push(`Add: "${inputValue}"`); - // } - return filtered; }} renderTags={(value, getTagProps) => diff --git a/frontend/src/pages/Home/index.tsx b/frontend/src/pages/Home/index.tsx index 7a2a5cdfaf..48621ee7a8 100644 --- a/frontend/src/pages/Home/index.tsx +++ b/frontend/src/pages/Home/index.tsx @@ -17,8 +17,8 @@ import AppMargin from "../../components/AppMargin"; import { complexityList, languageList, - maxMatchTimeout, - minMatchTimeout, + MAX_MATCH_TIMEOUT, + MIN_MATCH_TIMEOUT, QUESTION_DOES_NOT_EXIST_ERROR, USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; @@ -258,11 +258,11 @@ const Home: React.FC = () => { const newTimeout = value ? parseInt(value, 10) : undefined; setTimeout(newTimeout); }} - helperText={`Set a timeout between ${minMatchTimeout} to ${maxMatchTimeout} seconds`} + helperText={`Set a timeout between ${MIN_MATCH_TIMEOUT} to ${MAX_MATCH_TIMEOUT} seconds`} error={ !timeout || - timeout < minMatchTimeout || - timeout > maxMatchTimeout + timeout < MIN_MATCH_TIMEOUT || + timeout > MAX_MATCH_TIMEOUT } sx={{ backgroundColor: "white", @@ -283,8 +283,8 @@ const Home: React.FC = () => { sx={{ marginTop: 2 }} disabled={ !timeout || - timeout < minMatchTimeout || - timeout > maxMatchTimeout || + timeout < MIN_MATCH_TIMEOUT || + timeout > MAX_MATCH_TIMEOUT || !complexity || !category || !language diff --git a/frontend/src/pages/Matching/index.tsx b/frontend/src/pages/Matching/index.tsx index 03530bd56d..c0f6b297a6 100644 --- a/frontend/src/pages/Matching/index.tsx +++ b/frontend/src/pages/Matching/index.tsx @@ -6,7 +6,7 @@ import classes from "./index.module.css"; import Timer from "../../components/Timer"; import { useMatch } from "../../contexts/MatchContext"; import { - minMatchTimeout, + MIN_MATCH_TIMEOUT, USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; import { Navigate } from "react-router-dom"; @@ -17,7 +17,7 @@ const Matching: React.FC = () => { throw new Error(USE_MATCH_ERROR_MESSAGE); } const { matchingTimeout, matchCriteria } = match; - const timeout = matchCriteria?.timeout || minMatchTimeout; + const timeout = matchCriteria?.timeout || MIN_MATCH_TIMEOUT; const [timeLeft, setTimeLeft] = useState(timeout); diff --git a/frontend/src/reducers/questionReducer.ts b/frontend/src/reducers/questionReducer.ts index ada634bffd..0a0409e6f3 100644 --- a/frontend/src/reducers/questionReducer.ts +++ b/frontend/src/reducers/questionReducer.ts @@ -302,8 +302,6 @@ export const updateQuestionById = async ( description: question.description, complexity: question.complexity, category: question.categories, - // testcaseInputFileUrl: question.testcaseInputFileUrl, - // testcaseOutputFileUrl: question.testcaseOutputFileUrl, ...urls, pythonTemplate: question.pythonTemplate, javaTemplate: question.javaTemplate, diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 0cdb7441c0..9878795096 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -63,9 +63,8 @@ export const SUCCESSFUL_SIGNUP = // Field Validation export const FILL_ALL_FIELDS = "Please fill in all fields"; - -export const minMatchTimeout = 30; -export const maxMatchTimeout = 300; +export const MIN_MATCH_TIMEOUT = 30; +export const MAX_MATCH_TIMEOUT = 300; // Question export const SUCCESS_QUESTION_CREATE = "Question created successfully"; From 22819967293bbbd60054ed088e0e699c47a0d888 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 12 Nov 2024 13:36:28 +0800 Subject: [PATCH 172/192] Remove console.log --- backend/communication-service/src/handlers/websocketHandler.ts | 1 - frontend/src/contexts/CollabContext.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/backend/communication-service/src/handlers/websocketHandler.ts b/backend/communication-service/src/handlers/websocketHandler.ts index 959c80d64e..92658abdf2 100644 --- a/backend/communication-service/src/handlers/websocketHandler.ts +++ b/backend/communication-service/src/handlers/websocketHandler.ts @@ -9,7 +9,6 @@ export const handleWebsocketCommunicationEvents = (socket: Socket) => { CommunicationEvents.JOIN, async ({ roomId, username }: { roomId: string; username: string }) => { connectUser(username); - console.log(username, roomId); const room = io.sockets.adapter.rooms.get(roomId); if (room?.has(socket.id)) { socket.emit(CommunicationEvents.ALREADY_JOINED); diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 3167b81b4d..dc0f8e84ec 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -260,7 +260,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { if (!collabSocket.hasListeners(CollabEvents.SOCKET_DISCONNECT)) { collabSocket.on(CollabEvents.SOCKET_DISCONNECT, (reason) => { - console.log(reason); if ( reason !== CollabEvents.SOCKET_CLIENT_DISCONNECT && reason !== CollabEvents.SOCKET_SERVER_DISCONNECT From 64d4c127a478a20774858766111c9c4b58dd5e78 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 12 Nov 2024 15:04:12 +0800 Subject: [PATCH 173/192] Remove console.log --- backend/question-service/src/server.ts | 2 -- backend/user-service/src/model/repository.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/backend/question-service/src/server.ts b/backend/question-service/src/server.ts index ef42c5463d..a7f2bebe53 100644 --- a/backend/question-service/src/server.ts +++ b/backend/question-service/src/server.ts @@ -1,6 +1,5 @@ import app from "./app.ts"; import connectDB from "./config/db.ts"; -// import { seedQuestions } from "./scripts/seed.ts"; const PORT = process.env.SERVICE_PORT || 3000; @@ -8,7 +7,6 @@ if (process.env.NODE_ENV !== "test") { connectDB() .then(() => { console.log("MongoDB Connected!"); - // seedQuestions(); const server = app.listen(PORT, () => { console.log( diff --git a/backend/user-service/src/model/repository.ts b/backend/user-service/src/model/repository.ts index 10794ba664..a587c55cb7 100644 --- a/backend/user-service/src/model/repository.ts +++ b/backend/user-service/src/model/repository.ts @@ -10,8 +10,6 @@ export async function connectToDB() { ? process.env.MONGO_CLOUD_URI : process.env.MONGO_LOCAL_URI; - console.log(mongoDBUri); - if (!mongoDBUri) { throw new Error("MongoDB URI is not provided"); } From 74b8cd579c4f9e954db208898b72c1cd1562a8cd Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 12 Nov 2024 20:21:34 +0800 Subject: [PATCH 174/192] Fix rematch bug and log queue states --- .../matching-service/src/config/rabbitmq.ts | 29 ++++++++++++++----- frontend/src/contexts/MatchContext.tsx | 6 ++-- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/backend/matching-service/src/config/rabbitmq.ts b/backend/matching-service/src/config/rabbitmq.ts index 057d9755af..83cb5fe483 100644 --- a/backend/matching-service/src/config/rabbitmq.ts +++ b/backend/matching-service/src/config/rabbitmq.ts @@ -54,10 +54,25 @@ const setUpConsumer = async (queueName: string) => { consumerChannel.consume(queueName, (msg) => { if (msg !== null) { - const matchRequestItem = JSON.parse(msg.content.toString()); + const matchRequestItem = JSON.parse( + msg.content.toString() + ) as MatchRequestItem; const waitingList = getWaitingList(queueName); const [complexity, category] = deconstructQueueName(queueName); + console.log( + `Consumed from ${queueName}: ${JSON.stringify(matchRequestItem)}` + ); + console.log( + `Waiting list before matching: ${JSON.stringify([ + ...waitingList.entries(), + ])}` + ); matchUsers(matchRequestItem, waitingList, complexity, category); + console.log( + `Waiting list after matching: ${JSON.stringify([ + ...waitingList.entries(), + ])}` + ); consumerChannel.ack(msg); } }); @@ -70,13 +85,11 @@ const routeToQueue = async ( try { const queueName = constructQueueName(criterias); const senderChannel = await mrConnection.createChannel(); - senderChannel.sendToQueue( - queueName, - Buffer.from(JSON.stringify(requestItem)), - { - persistent: true, - } - ); + const msg = JSON.stringify(requestItem); + senderChannel.sendToQueue(queueName, Buffer.from(msg), { + persistent: true, + }); + console.log(`Sent to ${queueName}: ${msg}`); return true; } catch (error) { console.log(error); diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 49a43d7047..0f6e1fb164 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -409,9 +409,9 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const rematchRequest = { user: matchUser, - complexities: matchCriteria.complexity, - categories: matchCriteria.category, - languages: matchCriteria.language, + complexity: matchCriteria.complexity, + category: matchCriteria.category, + language: matchCriteria.language, timeout: matchCriteria.timeout, }; matchSocket.emit( From 1beb4f420026e09a41d728370d3bfda5ffad6272 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Tue, 12 Nov 2024 23:31:27 +0800 Subject: [PATCH 175/192] Fix sockets auth --- frontend/src/components/Chat/index.tsx | 60 ++--- frontend/src/components/CodeEditor/index.tsx | 40 +-- .../CollabSessionControls/index.tsx | 14 +- frontend/src/components/Navbar/index.tsx | 5 +- frontend/src/contexts/CollabContext.tsx | 232 +++++++++++++++--- frontend/src/contexts/MatchContext.tsx | 66 ++--- frontend/src/pages/CollabSandbox/index.tsx | 24 +- frontend/src/utils/collabCursor.ts | 5 +- frontend/src/utils/collabSocket.ts | 149 +---------- frontend/src/utils/communicationSocket.ts | 17 +- frontend/src/utils/constants.ts | 2 + frontend/src/utils/matchSocket.ts | 15 +- 12 files changed, 350 insertions(+), 279 deletions(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 7bea1c2770..c3fe54934e 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -1,16 +1,13 @@ import { Box, styled, TextField, Typography } from "@mui/material"; import { useEffect, useRef, useState } from "react"; -import { - CommunicationEvents, - communicationSocket, -} from "../../utils/communicationSocket"; +import { CommunicationEvents } from "../../utils/communicationSocket"; import { useMatch } from "../../contexts/MatchContext"; import { - USE_AUTH_ERROR_MESSAGE, + USE_COLLAB_ERROR_MESSAGE, USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; -import { useAuth } from "../../contexts/AuthContext"; import { toast } from "react-toastify"; +import { useCollab } from "../../contexts/CollabContext"; type Message = { from: string; @@ -34,33 +31,32 @@ const StyledTypography = styled(Typography)(({ theme }) => ({ const Chat: React.FC = ({ isActive }) => { const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(""); - const match = useMatch(); - const auth = useAuth(); + const messagesRef = useRef(null); const errorHandledRef = useRef(false); + const match = useMatch(); if (!match) { throw new Error(USE_MATCH_ERROR_MESSAGE); } + const { getMatchId, matchUser } = match; - if (!auth) { - throw new Error(USE_AUTH_ERROR_MESSAGE); + const collab = useCollab(); + if (!collab) { + throw new Error(USE_COLLAB_ERROR_MESSAGE); } - - const { getMatchId } = match; - const { user } = auth; + const { communicationSocket } = collab; useEffect(() => { // join the room automatically when this loads - communicationSocket.open(); - communicationSocket.emit(CommunicationEvents.JOIN, { + communicationSocket?.open(); + communicationSocket?.emit(CommunicationEvents.JOIN, { roomId: getMatchId(), - username: user?.username, + username: matchUser?.username, }); return () => { - communicationSocket.emit(CommunicationEvents.USER_DISCONNECT); - // setMessages([]); // clear the earlier messages in dev mode + communicationSocket?.emit(CommunicationEvents.USER_DISCONNECT); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -77,19 +73,25 @@ const Chat: React.FC = ({ isActive }) => { } }; - communicationSocket.on(CommunicationEvents.USER_JOINED, listener); - communicationSocket.on(CommunicationEvents.TEXT_MESSAGE_RECEIVED, listener); - communicationSocket.on(CommunicationEvents.DISCONNECTED, listener); - communicationSocket.on(CommunicationEvents.CONNECT_ERROR, errorListener); + communicationSocket?.on(CommunicationEvents.USER_JOINED, listener); + communicationSocket?.on( + CommunicationEvents.TEXT_MESSAGE_RECEIVED, + listener + ); + communicationSocket?.on(CommunicationEvents.DISCONNECTED, listener); + communicationSocket?.on(CommunicationEvents.CONNECT_ERROR, errorListener); return () => { - communicationSocket.off(CommunicationEvents.USER_JOINED, listener); - communicationSocket.off( + communicationSocket?.off(CommunicationEvents.USER_JOINED, listener); + communicationSocket?.off( CommunicationEvents.TEXT_MESSAGE_RECEIVED, listener ); - communicationSocket.off(CommunicationEvents.DISCONNECTED, listener); - communicationSocket.off(CommunicationEvents.CONNECT_ERROR, errorListener); + communicationSocket?.off(CommunicationEvents.DISCONNECTED, listener); + communicationSocket?.off( + CommunicationEvents.CONNECT_ERROR, + errorListener + ); }; }, []); @@ -137,7 +139,7 @@ const Chat: React.FC = ({ isActive }) => { {msg.message}
- ) : msg.from === user?.username ? ( + ) : msg.from === matchUser?.username ? ( ({ @@ -185,10 +187,10 @@ const Chat: React.FC = ({ isActive }) => { const trimmedValue = inputValue.trim(); if (e.key === "Enter" && !e.shiftKey && trimmedValue !== "") { e.preventDefault(); - communicationSocket.emit(CommunicationEvents.SEND_TEXT_MESSAGE, { + communicationSocket?.emit(CommunicationEvents.SEND_TEXT_MESSAGE, { roomId: getMatchId(), message: trimmedValue, - username: user?.username, + username: matchUser?.username, createdTime: Date.now(), }); setInputValue(""); diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index f74b69aa80..0df13ff076 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -5,17 +5,18 @@ import { EditorView } from "@codemirror/view"; import { EditorState } from "@codemirror/state"; import { indentUnit } from "@codemirror/language"; import { useEffect, useState } from "react"; -import { initDocument } from "../../utils/collabSocket"; import { cursorExtension } from "../../utils/collabCursor"; import { yCollab } from "y-codemirror.next"; import { Doc, Text } from "yjs"; import { Awareness } from "y-protocols/awareness"; import { useCollab } from "../../contexts/CollabContext"; import { + COLLAB_DOCUMENT_INIT_ERROR, USE_COLLAB_ERROR_MESSAGE, USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; import { useMatch } from "../../contexts/MatchContext"; +import { toast } from "react-toastify"; interface CodeEditorProps { editorState?: { doc: Doc; text: Text; awareness: Awareness }; @@ -57,7 +58,8 @@ const CodeEditor: React.FC = (props) => { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { checkDocReady } = collab; + const { checkDocReady, initDocument, sendCursorUpdate, receiveCursorUpdate } = + collab; const [isEditorReady, setIsEditorReady] = useState(false); const [isDocumentLoaded, setIsDocumentLoaded] = useState(false); @@ -82,17 +84,21 @@ const CodeEditor: React.FC = (props) => { questionTitle ) { checkDocReady(roomId, editorState.doc, setIsDocumentLoaded); - await initDocument( - uid, - roomId, - template, - matchUser.id, - partner.id, - matchCriteria.language, - questionId, - questionTitle - ); - setIsDocumentLoaded(true); + try { + await initDocument( + uid, + roomId, + template, + matchUser.id, + partner.id, + matchCriteria.language, + questionId, + questionTitle + ); + setIsDocumentLoaded(true); + } catch { + toast.error(COLLAB_DOCUMENT_INIT_ERROR); + } } }; loadTemplate(); @@ -114,7 +120,13 @@ const CodeEditor: React.FC = (props) => { ...(!isReadOnly && editorState ? [ yCollab(editorState.text, editorState.awareness), - cursorExtension(roomId, uid, username), + cursorExtension( + roomId, + uid, + username, + sendCursorUpdate, + receiveCursorUpdate + ), ] : []), EditorView.lineWrapping, diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index cbf2bc39e6..2c794e657b 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -1,6 +1,6 @@ import { Button, Stack } from "@mui/material"; import Stopwatch from "../Stopwatch"; -import { useCollab } from "../../contexts/CollabContext"; +import { CollabEvents, useCollab } from "../../contexts/CollabContext"; import { COLLAB_ENDED_MESSAGE, COLLAB_PARTNER_DISCONNECTED_MESSAGE, @@ -13,7 +13,6 @@ import { extractMinutesFromTime, extractSecondsFromTime, } from "../../utils/sessionTime"; -import { CollabEvents, collabSocket } from "../../utils/collabSocket"; import { toast } from "react-toastify"; import reducer, { getQuestionById, @@ -35,11 +34,12 @@ const CollabSessionControls: React.FC = () => { } const { + collabSocket, handleSubmitSessionClick, handleEndSessionClick, handleConfirmEndSession, - isEndSessionModalOpen, handleRejectEndSession, + isEndSessionModalOpen, handleExitSession, isExitSessionModalOpen, qnHistoryId, @@ -54,21 +54,21 @@ const CollabSessionControls: React.FC = () => { const { selectedQuestion } = state; useEffect(() => { - collabSocket.once(CollabEvents.END_SESSION, (sessionDuration: number) => { + collabSocket?.once(CollabEvents.END_SESSION, (sessionDuration: number) => { collabSocket.off(CollabEvents.PARTNER_DISCONNECTED); toast.info(COLLAB_ENDED_MESSAGE); handleConfirmEndSession(timeRef.current, setTime, true, sessionDuration); }); - collabSocket.once(CollabEvents.PARTNER_DISCONNECTED, () => { + collabSocket?.once(CollabEvents.PARTNER_DISCONNECTED, () => { collabSocket.off(CollabEvents.END_SESSION); toast.error(COLLAB_PARTNER_DISCONNECTED_MESSAGE); handleConfirmEndSession(timeRef.current, setTime, true); }); return () => { - collabSocket.off(CollabEvents.END_SESSION); - collabSocket.off(CollabEvents.PARTNER_DISCONNECTED); + collabSocket?.off(CollabEvents.END_SESSION); + collabSocket?.off(CollabEvents.PARTNER_DISCONNECTED); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index d3e74e8e78..3b6edcec09 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -14,7 +14,7 @@ import { } from "@mui/material"; import { grey } from "@mui/material/colors"; import AppMargin from "../AppMargin"; -import { useNavigate, useLocation } from "react-router-dom"; +import { useNavigate, useLocation, Link as RouterLink } from "react-router-dom"; import { useAuth } from "../../contexts/AuthContext"; import { useState } from "react"; import { @@ -93,8 +93,9 @@ const Navbar: React.FC = (props) => { .filter((item) => !item.needsLogin || (item.needsLogin && user)) .map((item) => ( diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 3167b81b4d..6c85be46cd 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -25,18 +25,47 @@ import { codeExecutionClient, qnHistoryClient } from "../utils/api"; import { useReducer } from "react"; import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; -import { - CollabEvents, - collabSocket, - getDocContent, - leave, -} from "../utils/collabSocket"; -import { - CommunicationEvents, - communicationSocket, -} from "../utils/communicationSocket"; +import { createCollabSocket } from "../utils/collabSocket"; import useAppNavigate from "../hooks/useAppNavigate"; -import { applyUpdateV2, Doc } from "yjs"; +import { applyUpdateV2, Doc, Text } from "yjs"; +import { Socket } from "socket.io-client"; +import { Awareness } from "y-protocols/awareness.js"; +import { Cursor, updateCursor } from "../utils/collabCursor"; +import { EditorView } from "@uiw/react-codemirror"; +import { createCommunicationSocket } from "../utils/communicationSocket"; + +export enum CollabEvents { + // Send + JOIN = "join", + LEAVE = "leave", + INIT_DOCUMENT = "init_document", + UPDATE_REQUEST = "update_request", + UPDATE_CURSOR_REQUEST = "update_cursor_request", + RECONNECT_REQUEST = "reconnect_request", + END_SESSION_REQUEST = "end_session_request", + + // Receive + ROOM_READY = "room_ready", + DOCUMENT_READY = "document_ready", + DOCUMENT_NOT_FOUND = "document_not_found", + UPDATE = "updateV2", + UPDATE_CURSOR = "update_cursor", + END_SESSION = "end_session", + PARTNER_DISCONNECTED = "partner_disconnected", + + SOCKET_DISCONNECT = "disconnect", + SOCKET_CLIENT_DISCONNECT = "io client disconnect", + SOCKET_SERVER_DISCONNECT = "io server disconnect", + SOCKET_RECONNECT_SUCCESS = "reconnect", + SOCKET_RECONNECT_FAILED = "reconnect_failed", +} + +export type CollabSessionData = { + ready: boolean; + doc: Doc; + text: Text; + awareness: Awareness; +}; export type CompilerResult = { status: string; @@ -52,6 +81,22 @@ export type CompilerResult = { }; type CollabContextType = { + collabSocket: Socket | null; + communicationSocket: Socket | null; + join: (uid: string, roomId: string) => Promise; + initDocument: ( + uid: string, + roomId: string, + template: string, + uid1: string, + uid2: string, + language: string, + qnId: string, + qnTitle: string + ) => Promise; + leave: (uid: string, roomId: string, isPartnerNotified: boolean) => void; + sendCursorUpdate: (roomId: string, cursor: Cursor) => void; + receiveCursorUpdate: (view: EditorView) => void; handleSubmitSessionClick: (time: number) => void; handleEndSessionClick: () => void; handleRejectEndSession: () => void; @@ -61,9 +106,9 @@ type CollabContextType = { isInitiatedByPartner: boolean, sessionDuration?: number ) => void; + isEndSessionModalOpen: boolean; compilerResult: CompilerResult[]; setCompilerResult: React.Dispatch>; - isEndSessionModalOpen: boolean; resetCollab: () => void; checkDocReady: ( roomId: string, @@ -104,13 +149,140 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { useState(false); const [qnHistoryId, setQnHistoryId] = useState(null); const [stopTime, setStopTime] = useState(true); + const [collabSessionData, setCollabSessionData] = + useState(null); + // sockets + const [collabSocket, setCollabSocket] = useState(null); + const [communicationSocket, setCommunicationSocket] = useState( + null + ); + + // refs + const collabSessionDataRef = useRef(null); const qnHistoryIdRef = useRef(qnHistoryId); + useEffect(() => { + if (matchUser) { + setCollabSocket(createCollabSocket()); + setCommunicationSocket(createCommunicationSocket()); + } else { + setCollabSocket(null); + setCommunicationSocket(null); + } + }, [matchUser]); + + useEffect(() => { + collabSessionDataRef.current = collabSessionData; + }, [collabSessionData]); + useEffect(() => { qnHistoryIdRef.current = qnHistoryId; }, [qnHistoryId]); + const join = (uid: string, roomId: string): Promise => { + collabSocket?.connect(); + + const doc = new Doc(); + const text = doc.getText(); + const awareness = new Awareness(doc); + setCollabSessionData({ + ready: false, + doc: doc, + text: text, + awareness: awareness, + }); + + doc.on(CollabEvents.UPDATE, (update, origin) => { + if (origin !== uid) { + collabSocket?.emit(CollabEvents.UPDATE_REQUEST, roomId, update); + } + }); + + collabSocket?.on(CollabEvents.UPDATE, (update) => { + applyUpdateV2(doc, new Uint8Array(update), uid); + }); + + collabSocket?.emit(CollabEvents.JOIN, uid, roomId); + + return new Promise((resolve) => { + collabSocket?.once(CollabEvents.ROOM_READY, (ready: boolean) => { + resolve({ ready: ready, doc: doc, text: text, awareness: awareness }); + }); + }); + }; + + const initDocument = ( + uid: string, + roomId: string, + template: string, + uid1: string, + uid2: string, + language: string, + qnId: string, + qnTitle: string + ) => { + collabSocket?.emit( + CollabEvents.INIT_DOCUMENT, + roomId, + template, + uid1, + uid2, + language, + qnId, + qnTitle + ); + + return new Promise((resolve, reject) => { + collabSocket?.once(CollabEvents.UPDATE, (update) => { + if (collabSessionDataRef.current) { + applyUpdateV2( + collabSessionDataRef.current.doc, + new Uint8Array(update), + uid + ); + resolve(); + } else { + reject(); + } + }); + }); + }; + + const leave = (uid: string, roomId: string, isPartnerNotified: boolean) => { + collabSocket?.removeAllListeners(); + collabSocket?.io.removeListener(CollabEvents.SOCKET_RECONNECT_SUCCESS); + collabSocket?.io.removeListener(CollabEvents.SOCKET_RECONNECT_FAILED); + collabSessionDataRef.current?.doc.destroy(); + + if (collabSocket?.connected) { + collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isPartnerNotified); + } + }; + + const sendCursorUpdate = (roomId: string, cursor: Cursor) => { + collabSocket?.emit(CollabEvents.UPDATE_CURSOR_REQUEST, roomId, cursor); + }; + + const receiveCursorUpdate = (view: EditorView) => { + if (collabSocket?.hasListeners(CollabEvents.UPDATE_CURSOR)) { + return; + } + + collabSocket?.on(CollabEvents.UPDATE_CURSOR, (cursor: Cursor) => { + view.dispatch({ + effects: updateCursor.of(cursor), + }); + }); + }; + + const getDocContent = () => { + const doc = collabSessionDataRef.current?.doc; + return doc && !doc.isDestroyed + ? doc.getText().toString().replace(/\t/g, " ".repeat(4)) // Replace tabs with 4 spaces to prevent formatting issues + : ""; + }; + const handleSubmitSessionClick = async (time: number) => { const code = getDocContent(); try { @@ -206,14 +378,11 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { if (!isInitiatedByPartner) { // Notify partner - collabSocket.emit(CollabEvents.END_SESSION_REQUEST, roomId, time); + collabSocket?.emit(CollabEvents.END_SESSION_REQUEST, roomId, time); } // Leave collaboration room leave(matchUser.id, roomId, true); - - // Leave chat room - communicationSocket.emit(CommunicationEvents.USER_DISCONNECT); }; const handleExitSession = () => { @@ -230,14 +399,14 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { doc: Doc, setIsDocumentLoaded: React.Dispatch> ) => { - if (!collabSocket.hasListeners(CollabEvents.DOCUMENT_READY)) { - collabSocket.on(CollabEvents.DOCUMENT_READY, (qnHistoryId: string) => { + if (!collabSocket?.hasListeners(CollabEvents.DOCUMENT_READY)) { + collabSocket?.on(CollabEvents.DOCUMENT_READY, (qnHistoryId: string) => { setQnHistoryId(qnHistoryId); }); } - if (!collabSocket.hasListeners(CollabEvents.DOCUMENT_NOT_FOUND)) { - collabSocket.on(CollabEvents.DOCUMENT_NOT_FOUND, () => { + if (!collabSocket?.hasListeners(CollabEvents.DOCUMENT_NOT_FOUND)) { + collabSocket?.on(CollabEvents.DOCUMENT_NOT_FOUND, () => { toast.error(COLLAB_DOCUMENT_ERROR); setIsDocumentLoaded(false); setStopTime(true); @@ -258,8 +427,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); } - if (!collabSocket.hasListeners(CollabEvents.SOCKET_DISCONNECT)) { - collabSocket.on(CollabEvents.SOCKET_DISCONNECT, (reason) => { + if (!collabSocket?.hasListeners(CollabEvents.SOCKET_DISCONNECT)) { + collabSocket?.on(CollabEvents.SOCKET_DISCONNECT, (reason) => { console.log(reason); if ( reason !== CollabEvents.SOCKET_CLIENT_DISCONNECT && @@ -272,8 +441,8 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); } - if (!collabSocket.io.hasListeners(CollabEvents.SOCKET_RECONNECT_SUCCESS)) { - collabSocket.io.on(CollabEvents.SOCKET_RECONNECT_SUCCESS, () => { + if (!collabSocket?.io.hasListeners(CollabEvents.SOCKET_RECONNECT_SUCCESS)) { + collabSocket?.io.on(CollabEvents.SOCKET_RECONNECT_SUCCESS, () => { const text = doc.getText(); doc.transact(() => { text.delete(0, text.length); @@ -290,14 +459,13 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); } - if (!collabSocket.io.hasListeners(CollabEvents.SOCKET_RECONNECT_FAILED)) { - collabSocket.io.on(CollabEvents.SOCKET_RECONNECT_FAILED, () => { + if (!collabSocket?.io.hasListeners(CollabEvents.SOCKET_RECONNECT_FAILED)) { + collabSocket?.io.on(CollabEvents.SOCKET_RECONNECT_FAILED, () => { toast.error(COLLAB_RECONNECTION_ERROR); if (matchUser) { leave(matchUser.id, roomId, true); } - communicationSocket.emit(CommunicationEvents.USER_DISCONNECT); handleExitSession(); }); @@ -309,18 +477,26 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setIsEndSessionModalOpen(false); setIsExitSessionModalOpen(false); setQnHistoryId(null); + setCollabSessionData(null); }; return ( = (props) => { const [loading, setLoading] = useState(true); const [questionId, setQuestionId] = useState(null); const [questionTitle, setQuestionTitle] = useState(null); + const [matchSocket, setMatchSocket] = useState(null); const navigator = useContext(UNSAFE_NavigationContext).navigator as History; @@ -123,8 +125,10 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { username: user.username, profile: user.profilePictureUrl, }); + setMatchSocket(createMatchSocket()); } else { setMatchUser(null); + setMatchSocket(null); } }, [user]); @@ -139,7 +143,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } openSocketConnection(); - matchSocket.emit(MatchEvents.USER_CONNECTED, matchUser?.id); + matchSocket?.emit(MatchEvents.USER_CONNECTED, matchUser?.id); const message = isMatchPage ? ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE @@ -187,19 +191,19 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const openSocketConnection = () => { - matchSocket.connect(); + matchSocket?.connect(); initListeners(); }; const closeSocketConnection = () => { - matchSocket.emit(MatchEvents.USER_DISCONNECTED, matchUser?.id); + matchSocket?.emit(MatchEvents.USER_DISCONNECTED, matchUser?.id); removeListeners(); }; const removeListeners = () => { - matchSocket.removeAllListeners(); - matchSocket.io.removeListener(MatchEvents.SOCKET_RECONNECT_SUCCESS); - matchSocket.io.removeListener(MatchEvents.SOCKET_RECONNECT_FAILED); + matchSocket?.removeAllListeners(); + matchSocket?.io.removeListener(MatchEvents.SOCKET_RECONNECT_SUCCESS); + matchSocket?.io.removeListener(MatchEvents.SOCKET_RECONNECT_FAILED); }; const initListeners = () => { @@ -222,8 +226,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const initConnectionStatusListeners = () => { let connectionLost = false; - if (!matchSocket.hasListeners(MatchEvents.SOCKET_DISCONNECT)) { - matchSocket.on(MatchEvents.SOCKET_DISCONNECT, (reason) => { + if (!matchSocket?.hasListeners(MatchEvents.SOCKET_DISCONNECT)) { + matchSocket?.on(MatchEvents.SOCKET_DISCONNECT, (reason) => { if ( reason !== MatchEvents.SOCKET_CLIENT_DISCONNECT && reason !== MatchEvents.SOCKET_SERVER_DISCONNECT @@ -233,8 +237,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); } - if (!matchSocket.io.hasListeners(MatchEvents.SOCKET_RECONNECT_SUCCESS)) { - matchSocket.io.on(MatchEvents.SOCKET_RECONNECT_SUCCESS, () => { + if (!matchSocket?.io.hasListeners(MatchEvents.SOCKET_RECONNECT_SUCCESS)) { + matchSocket?.io.on(MatchEvents.SOCKET_RECONNECT_SUCCESS, () => { if (connectionLost) { closeSocketConnection(); toast.error(MATCH_CONNECTION_ERROR); @@ -243,8 +247,8 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); } - if (!matchSocket.io.hasListeners(MatchEvents.SOCKET_RECONNECT_FAILED)) { - matchSocket.io.on(MatchEvents.SOCKET_RECONNECT_FAILED, () => { + if (!matchSocket?.io.hasListeners(MatchEvents.SOCKET_RECONNECT_FAILED)) { + matchSocket?.io.on(MatchEvents.SOCKET_RECONNECT_FAILED, () => { matchSocket.close(); toast.error(MATCH_CONNECTION_ERROR); appNavigate(MatchPaths.HOME); @@ -253,27 +257,27 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const initMatchRequestListeners = () => { - matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { + matchSocket?.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { handleMatchFound(matchId, user1, user2); }); - matchSocket.on(MatchEvents.MATCH_REQUEST_EXISTS, () => { + matchSocket?.on(MatchEvents.MATCH_REQUEST_EXISTS, () => { toast.error(MATCH_REQUEST_EXISTS_MESSAGE); }); - matchSocket.on(MatchEvents.MATCH_REQUEST_ERROR, () => { + matchSocket?.on(MatchEvents.MATCH_REQUEST_ERROR, () => { toast.error(FAILED_MATCH_REQUEST_MESSAGE); }); }; const initMatchingListeners = () => { - matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { + matchSocket?.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { handleMatchFound(matchId, user1, user2); }); }; const initMatchedListeners = () => { - matchSocket.on( + matchSocket?.on( MatchEvents.MATCH_SUCCESSFUL, (qnId: string, title: string) => { setMatchPending(false); @@ -283,16 +287,16 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } ); - matchSocket.on(MatchEvents.MATCH_UNSUCCESSFUL, () => { + matchSocket?.on(MatchEvents.MATCH_UNSUCCESSFUL, () => { toast.error(MATCH_UNSUCCESSFUL_MESSAGE); setMatchPending(false); }); - matchSocket.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { + matchSocket?.on(MatchEvents.MATCH_FOUND, ({ matchId, user1, user2 }) => { handleMatchFound(matchId, user1, user2); }); - matchSocket.on(MatchEvents.MATCH_REQUEST_ERROR, () => { + matchSocket?.on(MatchEvents.MATCH_REQUEST_ERROR, () => { toast.error(FAILED_MATCH_REQUEST_MESSAGE); }); }; @@ -330,7 +334,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setLoading(true); openSocketConnection(); - matchSocket.emit( + matchSocket?.emit( MatchEvents.MATCH_REQUEST, { user: matchUser, @@ -364,11 +368,11 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { appNavigate(MatchPaths.HOME); return; case MatchPaths.MATCHING: - matchSocket.emit(MatchEvents.MATCH_CANCEL_REQUEST, matchUser?.id); + matchSocket?.emit(MatchEvents.MATCH_CANCEL_REQUEST, matchUser?.id); appNavigate(MatchPaths.HOME); return; case MatchPaths.MATCHED: - matchSocket.emit( + matchSocket?.emit( MatchEvents.MATCH_DECLINE_REQUEST, matchUser?.id, matchId, @@ -377,7 +381,11 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { appNavigate(MatchPaths.HOME); return; case MatchPaths.COLLAB: - matchSocket.emit(MatchEvents.MATCH_END_REQUEST, matchUser?.id, matchId); + matchSocket?.emit( + MatchEvents.MATCH_END_REQUEST, + matchUser?.id, + matchId + ); return; default: return; @@ -390,7 +398,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { return; } - matchSocket.emit(MatchEvents.MATCH_ACCEPT_REQUEST, matchId); + matchSocket?.emit(MatchEvents.MATCH_ACCEPT_REQUEST, matchId); }; const rematch = () => { @@ -414,7 +422,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { languages: matchCriteria.language, timeout: matchCriteria.timeout, }; - matchSocket.emit( + matchSocket?.emit( MatchEvents.REMATCH_REQUEST, matchId, partner?.id, @@ -445,12 +453,12 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const matchingTimeout = () => { - matchSocket.emit(MatchEvents.MATCH_CANCEL_REQUEST, matchUser?.id); + matchSocket?.emit(MatchEvents.MATCH_CANCEL_REQUEST, matchUser?.id); appNavigate(MatchPaths.TIMEOUT); }; const matchOfferTimeout = () => { - matchSocket.emit( + matchSocket?.emit( MatchEvents.MATCH_DECLINE_REQUEST, matchUser?.id, matchId, diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 73ead18060..e0c0551831 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -1,7 +1,11 @@ import AppMargin from "../../components/AppMargin"; import { Box, Button, Grid2, Tab, Tabs } from "@mui/material"; import classes from "./index.module.css"; -import { CompilerResult, useCollab } from "../../contexts/CollabContext"; +import { + CollabSessionData, + CompilerResult, + useCollab, +} from "../../contexts/CollabContext"; import { useMatch } from "../../contexts/MatchContext"; import { COLLAB_CONNECTION_ERROR, @@ -20,7 +24,6 @@ import Chat from "../../components/Chat"; import TabPanel from "../../components/TabPanel"; import TestCase from "../../components/TestCase"; import CodeEditor from "../../components/CodeEditor"; -import { CollabSessionData, join, leave } from "../../utils/collabSocket"; import { toast } from "react-toastify"; const CollabSandbox: React.FC = () => { @@ -36,15 +39,14 @@ const CollabSandbox: React.FC = () => { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { compilerResult, resetCollab } = collab; + const { join, leave, compilerResult, resetCollab } = collab; const [state, dispatch] = useReducer(reducer, initialState); const { selectedQuestion } = state; const [selectedTab, setSelectedTab] = useState<"tests" | "chat">("tests"); const [selectedTestcase, setSelectedTestcase] = useState(0); - const [editorState, setEditorState] = useState( - null - ); + const [collabSessionData, setCollabSessionData] = + useState(null); const [isConnecting, setIsConnecting] = useState(true); const matchId = getMatchId(); @@ -59,9 +61,9 @@ const CollabSandbox: React.FC = () => { const connectToCollabSession = async () => { try { - const editorState = await join(matchUser.id, matchId); - if (editorState.ready) { - setEditorState(editorState); + const collabSessionData = await join(matchUser.id, matchId); + if (collabSessionData.ready) { + setCollabSessionData(collabSessionData); } else { toast.error(COLLAB_CONNECTION_ERROR); setIsConnecting(false); @@ -99,7 +101,7 @@ const CollabSandbox: React.FC = () => { return ; } - if (!selectedQuestion || !editorState || !compilerResult) { + if (!selectedQuestion || !collabSessionData || !compilerResult) { return ; } @@ -142,7 +144,7 @@ const CollabSandbox: React.FC = () => { })} > void, + receiveCursorUpdate: (view: EditorView) => void ) => { return [ cursorStateField(uid), // handles cursor positions and highlights diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 7d6461c201..276d4432be 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -1,149 +1,14 @@ -import { EditorView } from "@codemirror/view"; import { io } from "socket.io-client"; -import { updateCursor, Cursor } from "./collabCursor"; -import { Doc, Text, applyUpdateV2 } from "yjs"; -import { Awareness } from "y-protocols/awareness"; import { getToken } from "./token"; -export enum CollabEvents { - // Send - JOIN = "join", - LEAVE = "leave", - INIT_DOCUMENT = "init_document", - UPDATE_REQUEST = "update_request", - UPDATE_CURSOR_REQUEST = "update_cursor_request", - RECONNECT_REQUEST = "reconnect_request", - END_SESSION_REQUEST = "end_session_request", - - // Receive - ROOM_READY = "room_ready", - DOCUMENT_READY = "document_ready", - DOCUMENT_NOT_FOUND = "document_not_found", - UPDATE = "updateV2", - UPDATE_CURSOR = "update_cursor", - END_SESSION = "end_session", - PARTNER_DISCONNECTED = "partner_disconnected", - - SOCKET_DISCONNECT = "disconnect", - SOCKET_CLIENT_DISCONNECT = "io client disconnect", - SOCKET_SERVER_DISCONNECT = "io server disconnect", - SOCKET_RECONNECT_SUCCESS = "reconnect", - SOCKET_RECONNECT_FAILED = "reconnect_failed", -} - -export type CollabSessionData = { - ready: boolean; - doc: Doc; - text: Text; - awareness: Awareness; -}; - const COLLAB_SOCKET_URL = import.meta.env.VITE_COLLAB_SERVICE_URL ?? "http://localhost:3003"; -export const collabSocket = io(COLLAB_SOCKET_URL, { - reconnectionAttempts: 5, - autoConnect: false, - auth: { - token: getToken(), - }, -}); - -let doc: Doc; -let text: Text; -let awareness: Awareness; - -export const join = ( - uid: string, - roomId: string -): Promise => { - collabSocket.connect(); - - doc = new Doc(); - text = doc.getText(); - awareness = new Awareness(doc); - - doc.on(CollabEvents.UPDATE, (update, origin) => { - if (origin !== uid) { - collabSocket.emit(CollabEvents.UPDATE_REQUEST, roomId, update); - } - }); - - collabSocket.on(CollabEvents.UPDATE, (update) => { - applyUpdateV2(doc, new Uint8Array(update), uid); +export const createCollabSocket = () => + io(COLLAB_SOCKET_URL, { + reconnectionAttempts: 5, + autoConnect: false, + auth: { + token: getToken(), + }, }); - - collabSocket.emit(CollabEvents.JOIN, uid, roomId); - - return new Promise((resolve) => { - collabSocket.once(CollabEvents.ROOM_READY, (ready: boolean) => { - resolve({ ready: ready, doc: doc, text: text, awareness: awareness }); - }); - }); -}; - -export const initDocument = ( - uid: string, - roomId: string, - template: string, - uid1: string, - uid2: string, - language: string, - qnId: string, - qnTitle: string -) => { - collabSocket.emit( - CollabEvents.INIT_DOCUMENT, - roomId, - template, - uid1, - uid2, - language, - qnId, - qnTitle - ); - - return new Promise((resolve) => { - collabSocket.once(CollabEvents.UPDATE, (update) => { - applyUpdateV2(doc, new Uint8Array(update), uid); - resolve(); - }); - }); -}; - -export const leave = ( - uid: string, - roomId: string, - isPartnerNotified: boolean -) => { - collabSocket.removeAllListeners(); - collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_SUCCESS); - collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_FAILED); - doc?.destroy(); - - if (collabSocket.connected) { - collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isPartnerNotified); - } -}; - -export const sendCursorUpdate = (roomId: string, cursor: Cursor) => { - collabSocket.emit(CollabEvents.UPDATE_CURSOR_REQUEST, roomId, cursor); -}; - -export const receiveCursorUpdate = (view: EditorView) => { - if (collabSocket.hasListeners(CollabEvents.UPDATE_CURSOR)) { - return; - } - - collabSocket.on(CollabEvents.UPDATE_CURSOR, (cursor: Cursor) => { - view.dispatch({ - effects: updateCursor.of(cursor), - }); - }); -}; - -export const getDocContent = () => { - return doc && !doc.isDestroyed - ? doc.getText().toString().replace(/\t/g, " ".repeat(4)) // Replace tabs with 4 spaces to prevent formatting issues - : ""; -}; diff --git a/frontend/src/utils/communicationSocket.ts b/frontend/src/utils/communicationSocket.ts index 8356102b48..e1ef0dbf8b 100644 --- a/frontend/src/utils/communicationSocket.ts +++ b/frontend/src/utils/communicationSocket.ts @@ -19,11 +19,12 @@ export enum CommunicationEvents { const COMMUNICATION_SOCKET_URL = import.meta.env.VITE_COMM_SERVICE_URL ?? "http://localhost:3005"; -export const communicationSocket = io(COMMUNICATION_SOCKET_URL, { - reconnectionAttempts: 3, - autoConnect: false, - withCredentials: true, - auth: { - token: getToken(), - }, -}); +export const createCommunicationSocket = () => + io(COMMUNICATION_SOCKET_URL, { + reconnectionAttempts: 3, + autoConnect: false, + withCredentials: true, + auth: { + token: getToken(), + }, + }); diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts index 0cdb7441c0..99cbb587b2 100644 --- a/frontend/src/utils/constants.ts +++ b/frontend/src/utils/constants.ts @@ -119,6 +119,8 @@ export const COLLAB_DOCUMENT_ERROR = "Error syncing the code! Please wait as we try to reconnect. Recent changes may be lost."; export const COLLAB_DOCUMENT_RESTORED = "Connection restored! You may resume editing the code."; +export const COLLAB_DOCUMENT_INIT_ERROR = + "Error setting up the code editor! Please refresh this page or find another match."; // Code execution export const FAILED_TESTCASE_MESSAGE = diff --git a/frontend/src/utils/matchSocket.ts b/frontend/src/utils/matchSocket.ts index 754c96157c..777d88baa6 100644 --- a/frontend/src/utils/matchSocket.ts +++ b/frontend/src/utils/matchSocket.ts @@ -4,10 +4,11 @@ import { getToken } from "./token"; const MATCH_SOCKET_URL = import.meta.env.VITE_MATCH_SERVICE_URL ?? "http://localhost:3002"; -export const matchSocket = io(MATCH_SOCKET_URL, { - reconnectionAttempts: 3, - autoConnect: false, - auth: { - token: getToken(), - }, -}); +export const createMatchSocket = () => + io(MATCH_SOCKET_URL, { + reconnectionAttempts: 3, + autoConnect: false, + auth: { + token: getToken(), + }, + }); From 30d7a0a2dd3cc2e3d7a693fe25a7ff237e07aeed Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Wed, 13 Nov 2024 01:05:31 +0800 Subject: [PATCH 176/192] Remove useMatch in collab components --- frontend/src/components/Chat/index.tsx | 24 +- frontend/src/components/CodeEditor/index.tsx | 29 +- .../CollabSessionControls/index.tsx | 21 +- frontend/src/contexts/CollabContext.tsx | 315 ++++++++++-------- frontend/src/contexts/MatchContext.tsx | 38 +-- frontend/src/pages/CollabSandbox/index.tsx | 51 ++- frontend/src/utils/collabSocket.ts | 26 ++ frontend/src/utils/matchSocket.ts | 26 ++ 8 files changed, 278 insertions(+), 252 deletions(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index c3fe54934e..f6dc1825ed 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -1,11 +1,7 @@ import { Box, styled, TextField, Typography } from "@mui/material"; import { useEffect, useRef, useState } from "react"; import { CommunicationEvents } from "../../utils/communicationSocket"; -import { useMatch } from "../../contexts/MatchContext"; -import { - USE_COLLAB_ERROR_MESSAGE, - USE_MATCH_ERROR_MESSAGE, -} from "../../utils/constants"; +import { USE_COLLAB_ERROR_MESSAGE } from "../../utils/constants"; import { toast } from "react-toastify"; import { useCollab } from "../../contexts/CollabContext"; @@ -35,24 +31,18 @@ const Chat: React.FC = ({ isActive }) => { const messagesRef = useRef(null); const errorHandledRef = useRef(false); - const match = useMatch(); - if (!match) { - throw new Error(USE_MATCH_ERROR_MESSAGE); - } - const { getMatchId, matchUser } = match; - const collab = useCollab(); if (!collab) { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { communicationSocket } = collab; + const { communicationSocket, collabUser, roomId } = collab; useEffect(() => { // join the room automatically when this loads communicationSocket?.open(); communicationSocket?.emit(CommunicationEvents.JOIN, { - roomId: getMatchId(), - username: matchUser?.username, + roomId: roomId, + username: collabUser?.username, }); return () => { @@ -139,7 +129,7 @@ const Chat: React.FC = ({ isActive }) => { {msg.message}
- ) : msg.from === matchUser?.username ? ( + ) : msg.from === collabUser?.username ? ( ({ @@ -188,9 +178,9 @@ const Chat: React.FC = ({ isActive }) => { if (e.key === "Enter" && !e.shiftKey && trimmedValue !== "") { e.preventDefault(); communicationSocket?.emit(CommunicationEvents.SEND_TEXT_MESSAGE, { - roomId: getMatchId(), + roomId: roomId, message: trimmedValue, - username: matchUser?.username, + username: collabUser?.username, createdTime: Date.now(), }); setInputValue(""); diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 0df13ff076..1f04e707a5 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -50,16 +50,21 @@ const CodeEditor: React.FC = (props) => { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { matchCriteria, matchUser, partner, questionId, questionTitle } = - match; + const { partner, questionTitle } = match; const collab = useCollab(); if (!collab) { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { checkDocReady, initDocument, sendCursorUpdate, receiveCursorUpdate } = - collab; + const { + collabUser, + qnId, + initDocument, + checkDocReady, + sendCursorUpdate, + receiveCursorUpdate, + } = collab; const [isEditorReady, setIsEditorReady] = useState(false); const [isDocumentLoaded, setIsDocumentLoaded] = useState(false); @@ -76,29 +81,25 @@ const CodeEditor: React.FC = (props) => { } const loadTemplate = async () => { - if ( - matchUser && - partner && - matchCriteria && - questionId && - questionTitle - ) { + if (collabUser && partner && qnId && questionTitle) { checkDocReady(roomId, editorState.doc, setIsDocumentLoaded); try { await initDocument( uid, roomId, template, - matchUser.id, + collabUser.id, partner.id, - matchCriteria.language, - questionId, + language, + qnId, questionTitle ); setIsDocumentLoaded(true); } catch { toast.error(COLLAB_DOCUMENT_INIT_ERROR); } + } else { + toast.error(COLLAB_DOCUMENT_INIT_ERROR); } }; loadTemplate(); diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 2c794e657b..ef462af7a3 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -1,11 +1,10 @@ import { Button, Stack } from "@mui/material"; import Stopwatch from "../Stopwatch"; -import { CollabEvents, useCollab } from "../../contexts/CollabContext"; +import { useCollab } from "../../contexts/CollabContext"; import { COLLAB_ENDED_MESSAGE, COLLAB_PARTNER_DISCONNECTED_MESSAGE, USE_COLLAB_ERROR_MESSAGE, - USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; import { useEffect, useReducer, useRef, useState } from "react"; import CustomDialog from "../CustomDialog"; @@ -18,16 +17,9 @@ import reducer, { getQuestionById, initialState, } from "../../reducers/questionReducer"; -import { useMatch } from "../../contexts/MatchContext"; +import { CollabEvents } from "../../utils/collabSocket"; const CollabSessionControls: React.FC = () => { - const match = useMatch(); - if (!match) { - throw new Error(USE_MATCH_ERROR_MESSAGE); - } - - const { questionId } = match; - const collab = useCollab(); if (!collab) { throw new Error(USE_COLLAB_ERROR_MESSAGE); @@ -39,9 +31,10 @@ const CollabSessionControls: React.FC = () => { handleEndSessionClick, handleConfirmEndSession, handleRejectEndSession, - isEndSessionModalOpen, handleExitSession, + isEndSessionModalOpen, isExitSessionModalOpen, + qnId, qnHistoryId, stopTime, setStopTime, @@ -96,11 +89,11 @@ const CollabSessionControls: React.FC = () => { }, [qnHistoryId]); useEffect(() => { - if (!questionId) { + if (!qnId) { return; } - getQuestionById(questionId, dispatch); - }, [questionId]); + getQuestionById(qnId, dispatch); + }, [qnId]); return ( diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index 6c85be46cd..b6acbb8fa1 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -25,7 +25,7 @@ import { codeExecutionClient, qnHistoryClient } from "../utils/api"; import { useReducer } from "react"; import { updateQnHistoryById } from "../reducers/qnHistoryReducer"; import qnHistoryReducer, { initialQHState } from "../reducers/qnHistoryReducer"; -import { createCollabSocket } from "../utils/collabSocket"; +import { CollabEvents, createCollabSocket } from "../utils/collabSocket"; import useAppNavigate from "../hooks/useAppNavigate"; import { applyUpdateV2, Doc, Text } from "yjs"; import { Socket } from "socket.io-client"; @@ -34,31 +34,10 @@ import { Cursor, updateCursor } from "../utils/collabCursor"; import { EditorView } from "@uiw/react-codemirror"; import { createCommunicationSocket } from "../utils/communicationSocket"; -export enum CollabEvents { - // Send - JOIN = "join", - LEAVE = "leave", - INIT_DOCUMENT = "init_document", - UPDATE_REQUEST = "update_request", - UPDATE_CURSOR_REQUEST = "update_cursor_request", - RECONNECT_REQUEST = "reconnect_request", - END_SESSION_REQUEST = "end_session_request", - - // Receive - ROOM_READY = "room_ready", - DOCUMENT_READY = "document_ready", - DOCUMENT_NOT_FOUND = "document_not_found", - UPDATE = "updateV2", - UPDATE_CURSOR = "update_cursor", - END_SESSION = "end_session", - PARTNER_DISCONNECTED = "partner_disconnected", - - SOCKET_DISCONNECT = "disconnect", - SOCKET_CLIENT_DISCONNECT = "io client disconnect", - SOCKET_SERVER_DISCONNECT = "io server disconnect", - SOCKET_RECONNECT_SUCCESS = "reconnect", - SOCKET_RECONNECT_FAILED = "reconnect_failed", -} +type CollabUser = { + id: string; + username: string; +}; export type CollabSessionData = { ready: boolean; @@ -81,9 +60,13 @@ export type CompilerResult = { }; type CollabContextType = { + // Sockets collabSocket: Socket | null; communicationSocket: Socket | null; + + // Real-time logic join: (uid: string, roomId: string) => Promise; + leave: (uid: string, roomId: string, isPartnerNotified: boolean) => void; initDocument: ( uid: string, roomId: string, @@ -94,9 +77,15 @@ type CollabContextType = { qnId: string, qnTitle: string ) => Promise; - leave: (uid: string, roomId: string, isPartnerNotified: boolean) => void; + checkDocReady: ( + roomId: string, + doc: Doc, + setIsDocumentLoaded: React.Dispatch> + ) => void; sendCursorUpdate: (roomId: string, cursor: Cursor) => void; receiveCursorUpdate: (view: EditorView) => void; + + // End session logic handleSubmitSessionClick: (time: number) => void; handleEndSessionClick: () => void; handleRejectEndSession: () => void; @@ -106,20 +95,20 @@ type CollabContextType = { isInitiatedByPartner: boolean, sessionDuration?: number ) => void; - isEndSessionModalOpen: boolean; - compilerResult: CompilerResult[]; - setCompilerResult: React.Dispatch>; - resetCollab: () => void; - checkDocReady: ( - roomId: string, - doc: Doc, - setIsDocumentLoaded: React.Dispatch> - ) => void; handleExitSession: () => void; + isEndSessionModalOpen: boolean; isExitSessionModalOpen: boolean; + + // Collab session data + collabUser: CollabUser | null; + language: string | null; + roomId: string | null; + qnId: string | null; qnHistoryId: string | null; + compilerResult: CompilerResult[]; stopTime: boolean; setStopTime: React.Dispatch>; + resetCollab: () => void; }; const CollabContext = createContext(null); @@ -134,7 +123,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { matchUser, matchCriteria, getMatchId, stopMatch, questionId } = match; + const { matchId, matchUser, matchCriteria, questionId, stopMatch } = match; // eslint-disable-next-line const [_qnHistoryState, qnHistoryDispatch] = useReducer( @@ -142,28 +131,39 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { initialQHState ); - const [compilerResult, setCompilerResult] = useState([]); - const [isEndSessionModalOpen, setIsEndSessionModalOpen] = - useState(false); - const [isExitSessionModalOpen, setIsExitSessionModalOpen] = - useState(false); - const [qnHistoryId, setQnHistoryId] = useState(null); - const [stopTime, setStopTime] = useState(true); - const [collabSessionData, setCollabSessionData] = - useState(null); - - // sockets + // Sockets const [collabSocket, setCollabSocket] = useState(null); const [communicationSocket, setCommunicationSocket] = useState( null ); - // refs + // Session data + const [collabUser, setCollabUser] = useState(null); + const [language, setLanguage] = useState(null); + const [roomId, setRoomId] = useState(null); + const [qnId, setQnId] = useState(null); + const [qnHistoryId, setQnHistoryId] = useState(null); + const [compilerResult, setCompilerResult] = useState([]); + const [stopTime, setStopTime] = useState(true); + const [collabSessionData, setCollabSessionData] = + useState(null); + + // Refs const collabSessionDataRef = useRef(null); const qnHistoryIdRef = useRef(qnHistoryId); + // Modals + const [isEndSessionModalOpen, setIsEndSessionModalOpen] = + useState(false); + const [isExitSessionModalOpen, setIsExitSessionModalOpen] = + useState(false); + useEffect(() => { if (matchUser) { + setCollabUser({ + id: matchUser.id, + username: matchUser.username, + }); setCollabSocket(createCollabSocket()); setCommunicationSocket(createCommunicationSocket()); } else { @@ -173,13 +173,25 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }, [matchUser]); useEffect(() => { - collabSessionDataRef.current = collabSessionData; - }, [collabSessionData]); + setLanguage(matchCriteria?.language || null); + }, [matchCriteria]); + + useEffect(() => { + setRoomId(matchId); + }, [matchId]); + + useEffect(() => { + setQnId(questionId); + }, [questionId]); useEffect(() => { qnHistoryIdRef.current = qnHistoryId; }, [qnHistoryId]); + useEffect(() => { + collabSessionDataRef.current = collabSessionData; + }, [collabSessionData]); + const join = (uid: string, roomId: string): Promise => { collabSocket?.connect(); @@ -212,6 +224,17 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); }; + const leave = (uid: string, roomId: string, isPartnerNotified: boolean) => { + collabSocket?.removeAllListeners(); + collabSocket?.io.removeListener(CollabEvents.SOCKET_RECONNECT_SUCCESS); + collabSocket?.io.removeListener(CollabEvents.SOCKET_RECONNECT_FAILED); + collabSessionDataRef.current?.doc.destroy(); + + if (collabSocket?.connected) { + collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isPartnerNotified); + } + }; + const initDocument = ( uid: string, roomId: string, @@ -249,14 +272,81 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }); }; - const leave = (uid: string, roomId: string, isPartnerNotified: boolean) => { - collabSocket?.removeAllListeners(); - collabSocket?.io.removeListener(CollabEvents.SOCKET_RECONNECT_SUCCESS); - collabSocket?.io.removeListener(CollabEvents.SOCKET_RECONNECT_FAILED); - collabSessionDataRef.current?.doc.destroy(); + const checkDocReady = ( + roomId: string, + doc: Doc, + setIsDocumentLoaded: React.Dispatch> + ) => { + if (!collabSocket?.hasListeners(CollabEvents.DOCUMENT_READY)) { + collabSocket?.on(CollabEvents.DOCUMENT_READY, (qnHistoryId: string) => { + setQnHistoryId(qnHistoryId); + }); + } - if (collabSocket?.connected) { - collabSocket.emit(CollabEvents.LEAVE, uid, roomId, isPartnerNotified); + if (!collabSocket?.hasListeners(CollabEvents.DOCUMENT_NOT_FOUND)) { + collabSocket?.on(CollabEvents.DOCUMENT_NOT_FOUND, () => { + toast.error(COLLAB_DOCUMENT_ERROR); + setIsDocumentLoaded(false); + setStopTime(true); + + const text = doc.getText(); + doc.transact(() => { + text.delete(0, text.length); + }, collabUser?.id); + + collabSocket.once(CollabEvents.UPDATE, (update) => { + applyUpdateV2(doc, new Uint8Array(update), collabUser?.id); + toast.success(COLLAB_DOCUMENT_RESTORED); + setIsDocumentLoaded(true); + setStopTime(false); + }); + + collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); + }); + } + + if (!collabSocket?.hasListeners(CollabEvents.SOCKET_DISCONNECT)) { + collabSocket?.on(CollabEvents.SOCKET_DISCONNECT, (reason) => { + console.log(reason); + if ( + reason !== CollabEvents.SOCKET_CLIENT_DISCONNECT && + reason !== CollabEvents.SOCKET_SERVER_DISCONNECT + ) { + toast.error(COLLAB_DOCUMENT_ERROR); + setIsDocumentLoaded(false); + setStopTime(true); + } + }); + } + + if (!collabSocket?.io.hasListeners(CollabEvents.SOCKET_RECONNECT_SUCCESS)) { + collabSocket?.io.on(CollabEvents.SOCKET_RECONNECT_SUCCESS, () => { + const text = doc.getText(); + doc.transact(() => { + text.delete(0, text.length); + }, collabUser?.id); + + collabSocket.once(CollabEvents.UPDATE, (update) => { + applyUpdateV2(doc, new Uint8Array(update), collabUser?.id); + toast.success(COLLAB_DOCUMENT_RESTORED); + setIsDocumentLoaded(true); + setStopTime(false); + }); + + collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); + }); + } + + if (!collabSocket?.io.hasListeners(CollabEvents.SOCKET_RECONNECT_FAILED)) { + collabSocket?.io.on(CollabEvents.SOCKET_RECONNECT_FAILED, () => { + toast.error(COLLAB_RECONNECTION_ERROR); + + if (collabUser) { + leave(collabUser.id, roomId, true); + } + + handleExitSession(); + }); } }; @@ -287,9 +377,9 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const code = getDocContent(); try { const res = await codeExecutionClient.post("/", { - questionId, + questionId: qnId, code: code, - language: matchCriteria?.language.toLowerCase(), + language: language?.toLowerCase(), }); setCompilerResult([...res.data.data]); @@ -343,8 +433,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { ) => { setIsEndSessionModalOpen(false); - const roomId = getMatchId(); - if (!matchUser || !roomId || !qnHistoryIdRef.current) { + if (!collabUser || !roomId || !qnHistoryIdRef.current) { toast.error(COLLAB_END_ERROR); appNavigate("/home"); return; @@ -382,7 +471,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } // Leave collaboration room - leave(matchUser.id, roomId, true); + leave(collabUser.id, roomId, true); }; const handleExitSession = () => { @@ -394,84 +483,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { resetCollab(); }; - const checkDocReady = ( - roomId: string, - doc: Doc, - setIsDocumentLoaded: React.Dispatch> - ) => { - if (!collabSocket?.hasListeners(CollabEvents.DOCUMENT_READY)) { - collabSocket?.on(CollabEvents.DOCUMENT_READY, (qnHistoryId: string) => { - setQnHistoryId(qnHistoryId); - }); - } - - if (!collabSocket?.hasListeners(CollabEvents.DOCUMENT_NOT_FOUND)) { - collabSocket?.on(CollabEvents.DOCUMENT_NOT_FOUND, () => { - toast.error(COLLAB_DOCUMENT_ERROR); - setIsDocumentLoaded(false); - setStopTime(true); - - const text = doc.getText(); - doc.transact(() => { - text.delete(0, text.length); - }, matchUser?.id); - - collabSocket.once(CollabEvents.UPDATE, (update) => { - applyUpdateV2(doc, new Uint8Array(update), matchUser?.id); - toast.success(COLLAB_DOCUMENT_RESTORED); - setIsDocumentLoaded(true); - setStopTime(false); - }); - - collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); - }); - } - - if (!collabSocket?.hasListeners(CollabEvents.SOCKET_DISCONNECT)) { - collabSocket?.on(CollabEvents.SOCKET_DISCONNECT, (reason) => { - console.log(reason); - if ( - reason !== CollabEvents.SOCKET_CLIENT_DISCONNECT && - reason !== CollabEvents.SOCKET_SERVER_DISCONNECT - ) { - toast.error(COLLAB_DOCUMENT_ERROR); - setIsDocumentLoaded(false); - setStopTime(true); - } - }); - } - - if (!collabSocket?.io.hasListeners(CollabEvents.SOCKET_RECONNECT_SUCCESS)) { - collabSocket?.io.on(CollabEvents.SOCKET_RECONNECT_SUCCESS, () => { - const text = doc.getText(); - doc.transact(() => { - text.delete(0, text.length); - }, matchUser?.id); - - collabSocket.once(CollabEvents.UPDATE, (update) => { - applyUpdateV2(doc, new Uint8Array(update), matchUser?.id); - toast.success(COLLAB_DOCUMENT_RESTORED); - setIsDocumentLoaded(true); - setStopTime(false); - }); - - collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); - }); - } - - if (!collabSocket?.io.hasListeners(CollabEvents.SOCKET_RECONNECT_FAILED)) { - collabSocket?.io.on(CollabEvents.SOCKET_RECONNECT_FAILED, () => { - toast.error(COLLAB_RECONNECTION_ERROR); - - if (matchUser) { - leave(matchUser.id, roomId, true); - } - - handleExitSession(); - }); - } - }; - const resetCollab = () => { setCompilerResult([]); setIsEndSessionModalOpen(false); @@ -483,27 +494,37 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { return ( {children} diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 6a607a8788..f4e65ab136 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-refresh/only-export-components */ import React, { createContext, useContext, useEffect, useState } from "react"; -import { createMatchSocket } from "../utils/matchSocket"; +import { createMatchSocket, MatchEvents } from "../utils/matchSocket"; import { ABORT_COLLAB_SESSION_CONFIRMATION_MESSAGE, ABORT_MATCH_PROCESS_CONFIRMATION_MESSAGE, @@ -33,32 +33,6 @@ type MatchCriteria = { timeout: number; }; -enum MatchEvents { - // Send - MATCH_REQUEST = "match_request", - MATCH_CANCEL_REQUEST = "match_cancel_request", - MATCH_ACCEPT_REQUEST = "match_accept_request", - MATCH_DECLINE_REQUEST = "match_decline_request", - REMATCH_REQUEST = "rematch_request", - MATCH_END_REQUEST = "match_end_request", - - USER_CONNECTED = "user_connected", - USER_DISCONNECTED = "user_disconnected", - - // Receive - MATCH_FOUND = "match_found", - MATCH_SUCCESSFUL = "match_successful", - MATCH_UNSUCCESSFUL = "match_unsuccessful", - MATCH_REQUEST_EXISTS = "match_request_exists", - MATCH_REQUEST_ERROR = "match_request_error", - - SOCKET_DISCONNECT = "disconnect", - SOCKET_CLIENT_DISCONNECT = "io client disconnect", - SOCKET_SERVER_DISCONNECT = "io server disconnect", - SOCKET_RECONNECT_SUCCESS = "reconnect", - SOCKET_RECONNECT_FAILED = "reconnect_failed", -} - enum MatchPaths { HOME = "/home", TIMEOUT = "/matching/timeout", @@ -80,7 +54,7 @@ type MatchContextType = { retryMatch: () => void; matchingTimeout: () => void; matchOfferTimeout: () => void; - getMatchId: () => string | null; + matchId: string | null; matchUser: MatchUser | null; matchCriteria: MatchCriteria | null; partner: MatchUser | null; @@ -104,6 +78,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { } const { user } = auth; + const [matchSocket, setMatchSocket] = useState(null); const [matchUser, setMatchUser] = useState(null); const [matchCriteria, setMatchCriteria] = useState( null @@ -114,7 +89,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const [loading, setLoading] = useState(true); const [questionId, setQuestionId] = useState(null); const [questionTitle, setQuestionTitle] = useState(null); - const [matchSocket, setMatchSocket] = useState(null); const navigator = useContext(UNSAFE_NavigationContext).navigator as History; @@ -467,10 +441,6 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { appNavigate(MatchPaths.HOME); }; - const getMatchId = () => { - return matchId; - }; - return ( = (props) => { retryMatch, matchingTimeout, matchOfferTimeout, - getMatchId, + matchId, matchUser, matchCriteria, partner, diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index e0c0551831..c5bf6ed575 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -6,11 +6,9 @@ import { CompilerResult, useCollab, } from "../../contexts/CollabContext"; -import { useMatch } from "../../contexts/MatchContext"; import { COLLAB_CONNECTION_ERROR, USE_COLLAB_ERROR_MESSAGE, - USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; import { useEffect, useReducer, useState } from "react"; import Loader from "../../components/Loader"; @@ -27,19 +25,21 @@ import CodeEditor from "../../components/CodeEditor"; import { toast } from "react-toastify"; const CollabSandbox: React.FC = () => { - const match = useMatch(); - if (!match) { - throw new Error(USE_MATCH_ERROR_MESSAGE); - } - - const { getMatchId, matchUser, matchCriteria, questionId } = match; - const collab = useCollab(); if (!collab) { throw new Error(USE_COLLAB_ERROR_MESSAGE); } - const { join, leave, compilerResult, resetCollab } = collab; + const { + join, + leave, + resetCollab, + collabUser, + language, + roomId, + qnId, + compilerResult, + } = collab; const [state, dispatch] = useReducer(reducer, initialState); const { selectedQuestion } = state; @@ -48,12 +48,11 @@ const CollabSandbox: React.FC = () => { const [collabSessionData, setCollabSessionData] = useState(null); const [isConnecting, setIsConnecting] = useState(true); - const matchId = getMatchId(); useEffect(() => { resetCollab(); - if (!matchUser || !matchId) { + if (!collabUser || !roomId) { toast.error(COLLAB_CONNECTION_ERROR); setIsConnecting(false); return; @@ -61,7 +60,7 @@ const CollabSandbox: React.FC = () => { const connectToCollabSession = async () => { try { - const collabSessionData = await join(matchUser.id, matchId); + const collabSessionData = await join(collabUser.id, roomId); if (collabSessionData.ready) { setCollabSessionData(collabSessionData); } else { @@ -78,12 +77,12 @@ const CollabSandbox: React.FC = () => { // handle page refresh / tab closure const handleUnload = () => { - leave(matchUser.id, matchId, false); + leave(collabUser.id, roomId, false); }; window.addEventListener("unload", handleUnload); return () => { - leave(matchUser.id, matchId, false); + leave(collabUser.id, roomId, false); window.removeEventListener("unload", handleUnload); }; @@ -91,13 +90,13 @@ const CollabSandbox: React.FC = () => { }, []); useEffect(() => { - if (!questionId) { + if (!qnId) { return; } - getQuestionById(questionId, dispatch); - }, [questionId]); + getQuestionById(qnId, dispatch); + }, [qnId]); - if (!matchUser || !matchCriteria || !matchId || !isConnecting) { + if (!collabUser || !language || !roomId || !isConnecting) { return ; } @@ -145,19 +144,19 @@ const CollabSandbox: React.FC = () => { > Date: Wed, 13 Nov 2024 01:41:37 +0800 Subject: [PATCH 177/192] Remove useMatch in code editor --- frontend/src/components/CodeEditor/index.tsx | 17 +++------- frontend/src/contexts/CollabContext.tsx | 33 +++++++++++++++----- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index 1f04e707a5..e56d163690 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -13,9 +13,7 @@ import { useCollab } from "../../contexts/CollabContext"; import { COLLAB_DOCUMENT_INIT_ERROR, USE_COLLAB_ERROR_MESSAGE, - USE_MATCH_ERROR_MESSAGE, } from "../../utils/constants"; -import { useMatch } from "../../contexts/MatchContext"; import { toast } from "react-toastify"; interface CodeEditorProps { @@ -45,13 +43,6 @@ const CodeEditor: React.FC = (props) => { isReadOnly = false, } = props; - const match = useMatch(); - if (!match) { - throw new Error(USE_MATCH_ERROR_MESSAGE); - } - - const { partner, questionTitle } = match; - const collab = useCollab(); if (!collab) { throw new Error(USE_COLLAB_ERROR_MESSAGE); @@ -59,7 +50,9 @@ const CodeEditor: React.FC = (props) => { const { collabUser, + collabPartner, qnId, + qnTitle, initDocument, checkDocReady, sendCursorUpdate, @@ -81,7 +74,7 @@ const CodeEditor: React.FC = (props) => { } const loadTemplate = async () => { - if (collabUser && partner && qnId && questionTitle) { + if (collabUser && collabPartner && qnId && qnTitle) { checkDocReady(roomId, editorState.doc, setIsDocumentLoaded); try { await initDocument( @@ -89,10 +82,10 @@ const CodeEditor: React.FC = (props) => { roomId, template, collabUser.id, - partner.id, + collabPartner.id, language, qnId, - questionTitle + qnTitle ); setIsDocumentLoaded(true); } catch { diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index b6acbb8fa1..ea0956ea91 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -101,9 +101,11 @@ type CollabContextType = { // Collab session data collabUser: CollabUser | null; + collabPartner: CollabUser | null; language: string | null; roomId: string | null; qnId: string | null; + qnTitle: string | null; qnHistoryId: string | null; compilerResult: CompilerResult[]; stopTime: boolean; @@ -123,7 +125,15 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { throw new Error(USE_MATCH_ERROR_MESSAGE); } - const { matchId, matchUser, matchCriteria, questionId, stopMatch } = match; + const { + matchId, + matchUser, + matchCriteria, + partner, + questionId, + questionTitle, + stopMatch, + } = match; // eslint-disable-next-line const [_qnHistoryState, qnHistoryDispatch] = useReducer( @@ -139,9 +149,11 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { // Session data const [collabUser, setCollabUser] = useState(null); + const [collabPartner, setCollabPartner] = useState(null); const [language, setLanguage] = useState(null); const [roomId, setRoomId] = useState(null); const [qnId, setQnId] = useState(null); + const [qnTitle, setQnTitle] = useState(null); const [qnHistoryId, setQnHistoryId] = useState(null); const [compilerResult, setCompilerResult] = useState([]); const [stopTime, setStopTime] = useState(true); @@ -173,16 +185,19 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }, [matchUser]); useEffect(() => { + setCollabPartner( + partner + ? { + id: partner.id, + username: partner.username, + } + : null + ); setLanguage(matchCriteria?.language || null); - }, [matchCriteria]); - - useEffect(() => { setRoomId(matchId); - }, [matchId]); - - useEffect(() => { setQnId(questionId); - }, [questionId]); + setQnTitle(questionTitle); + }, [matchCriteria, matchId, questionId, questionTitle]); useEffect(() => { qnHistoryIdRef.current = qnHistoryId; @@ -517,9 +532,11 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { // Collab session data collabUser, + collabPartner, language, roomId, qnId, + qnTitle, qnHistoryId, compilerResult, stopTime, From 86c50ff08c38a743cf52d09261fad9e5d652c39a Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Wed, 13 Nov 2024 01:49:55 +0800 Subject: [PATCH 178/192] Add missing dependency --- frontend/src/contexts/CollabContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index ea0956ea91..f9edddc9ad 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -197,7 +197,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { setRoomId(matchId); setQnId(questionId); setQnTitle(questionTitle); - }, [matchCriteria, matchId, questionId, questionTitle]); + }, [partner, matchCriteria, matchId, questionId, questionTitle]); useEffect(() => { qnHistoryIdRef.current = qnHistoryId; From 1fcddd8d7e0f8da3f1470867ccae22b560097142 Mon Sep 17 00:00:00 2001 From: Tin Ruiqi Date: Wed, 13 Nov 2024 02:25:15 +0800 Subject: [PATCH 179/192] Remove duplicate uid --- frontend/src/components/CodeEditor/index.tsx | 23 +++++--------------- frontend/src/contexts/CollabContext.tsx | 4 +--- frontend/src/pages/CollabSandbox/index.tsx | 3 --- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx index e56d163690..9dc3b5ca72 100644 --- a/frontend/src/components/CodeEditor/index.tsx +++ b/frontend/src/components/CodeEditor/index.tsx @@ -18,11 +18,8 @@ import { toast } from "react-toastify"; interface CodeEditorProps { editorState?: { doc: Doc; text: Text; awareness: Awareness }; - uid?: string; - username?: string; language: string; template?: string; - roomId?: string; isReadOnly?: boolean; } @@ -33,15 +30,7 @@ const languageSupport = { }; const CodeEditor: React.FC = (props) => { - const { - editorState, - uid = "", - username = "", - language, - template = "", - roomId = "", - isReadOnly = false, - } = props; + const { editorState, language, template = "", isReadOnly = false } = props; const collab = useCollab(); if (!collab) { @@ -51,6 +40,7 @@ const CodeEditor: React.FC = (props) => { const { collabUser, collabPartner, + roomId, qnId, qnTitle, initDocument, @@ -74,11 +64,10 @@ const CodeEditor: React.FC = (props) => { } const loadTemplate = async () => { - if (collabUser && collabPartner && qnId && qnTitle) { + if (collabUser && collabPartner && roomId && qnId && qnTitle) { checkDocReady(roomId, editorState.doc, setIsDocumentLoaded); try { await initDocument( - uid, roomId, template, collabUser.id, @@ -111,13 +100,13 @@ const CodeEditor: React.FC = (props) => { indentUnit.of("\t"), basicSetup(), languageSupport[language as keyof typeof languageSupport], - ...(!isReadOnly && editorState + ...(!isReadOnly && editorState && roomId && collabUser ? [ yCollab(editorState.text, editorState.awareness), cursorExtension( roomId, - uid, - username, + collabUser.id, + collabUser.username, sendCursorUpdate, receiveCursorUpdate ), diff --git a/frontend/src/contexts/CollabContext.tsx b/frontend/src/contexts/CollabContext.tsx index f9edddc9ad..dca545c621 100644 --- a/frontend/src/contexts/CollabContext.tsx +++ b/frontend/src/contexts/CollabContext.tsx @@ -68,7 +68,6 @@ type CollabContextType = { join: (uid: string, roomId: string) => Promise; leave: (uid: string, roomId: string, isPartnerNotified: boolean) => void; initDocument: ( - uid: string, roomId: string, template: string, uid1: string, @@ -251,7 +250,6 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { }; const initDocument = ( - uid: string, roomId: string, template: string, uid1: string, @@ -277,7 +275,7 @@ const CollabProvider: React.FC<{ children?: React.ReactNode }> = (props) => { applyUpdateV2( collabSessionDataRef.current.doc, new Uint8Array(update), - uid + uid1 ); resolve(); } else { diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index c5bf6ed575..3f1a98f6fd 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -144,8 +144,6 @@ const CollabSandbox: React.FC = () => { > { ? selectedQuestion.cTemplate : "" } - roomId={roomId} /> Date: Wed, 13 Nov 2024 09:33:25 +0800 Subject: [PATCH 180/192] Update usematch mock --- frontend/src/components/Navbar/Navbar.test.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Navbar/Navbar.test.tsx b/frontend/src/components/Navbar/Navbar.test.tsx index 42a7dccb93..248e17f570 100644 --- a/frontend/src/components/Navbar/Navbar.test.tsx +++ b/frontend/src/components/Navbar/Navbar.test.tsx @@ -26,12 +26,14 @@ beforeEach(() => { retryMatch: jest.fn(), matchingTimeout: jest.fn(), matchOfferTimeout: jest.fn(), - verifyMatchStatus: jest.fn(), + matchId: null, matchUser: null, matchCriteria: null, partner: null, matchPending: false, loading: false, + questionId: null, + questionTitle: null, })); }); From 44e5aa39959b247c33340d37201ee85be36ef99b Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 13 Nov 2024 10:43:16 +0800 Subject: [PATCH 181/192] Add chat notificiation --- frontend/src/components/Chat/index.tsx | 4 +++- frontend/src/pages/CollabSandbox/index.tsx | 27 ++++++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 01554516fd..7fdcefba09 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -21,6 +21,7 @@ type Message = { type ChatProps = { isActive: boolean; + setHasNewMessage: React.Dispatch>; }; const StyledTypography = styled(Typography)(({ theme }) => ({ @@ -31,7 +32,7 @@ const StyledTypography = styled(Typography)(({ theme }) => ({ wordBreak: "break-word", })); -const Chat: React.FC = ({ isActive }) => { +const Chat: React.FC = ({ isActive, setHasNewMessage }) => { const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(""); const match = useMatch(); @@ -68,6 +69,7 @@ const Chat: React.FC = ({ isActive }) => { // initialize listener for incoming messages const listener = (message: Message) => { setMessages((prevMessages) => [...prevMessages, message]); + setHasNewMessage(true); }; const errorListener = () => { if (!errorHandledRef.current) { diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 73ead18060..d7ef2a4ba5 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -1,5 +1,5 @@ import AppMargin from "../../components/AppMargin"; -import { Box, Button, Grid2, Tab, Tabs } from "@mui/material"; +import { Badge, Box, Button, Grid2, Tab, Tabs } from "@mui/material"; import classes from "./index.module.css"; import { CompilerResult, useCollab } from "../../contexts/CollabContext"; import { useMatch } from "../../contexts/MatchContext"; @@ -41,6 +41,7 @@ const CollabSandbox: React.FC = () => { const [state, dispatch] = useReducer(reducer, initialState); const { selectedQuestion } = state; const [selectedTab, setSelectedTab] = useState<"tests" | "chat">("tests"); + const [hasNewMessage, setHasNewMessage] = useState(false); const [selectedTestcase, setSelectedTestcase] = useState(0); const [editorState, setEditorState] = useState( null @@ -169,7 +170,10 @@ const CollabSandbox: React.FC = () => { > setSelectedTab(value)} + onChange={(_, value) => { + setSelectedTab(value); + setHasNewMessage(false); + }} sx={(theme) => ({ position: "sticky", top: 0, @@ -179,7 +183,19 @@ const CollabSandbox: React.FC = () => { })} > - + + + Chat + + } + value="chat" + /> @@ -211,7 +227,10 @@ const CollabSandbox: React.FC = () => { /> - + From 52c92249c15e050980360ce5ca019afa775be993 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 13 Nov 2024 11:28:45 +0800 Subject: [PATCH 182/192] Update dockerignore and readme --- README.md | 20 ++++++++++++++++++++ backend/code-execution-service/.dockerignore | 1 + backend/collab-service/.dockerignore | 1 + backend/communication-service/.dockerignore | 1 + backend/matching-service/.dockerignore | 1 + backend/qn-history-service/.dockerignore | 1 + backend/question-service/.dockerignore | 1 + backend/user-service/.dockerignore | 1 + frontend/.dockerignore | 1 + 9 files changed, 28 insertions(+) diff --git a/README.md b/README.md index 3414a2e46b..4575367929 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,26 @@ To stop all the services, use the following command: docker-compose down ``` +## Running in Production Mode + +1. Build all the services (without using cache). + +``` +docker-compose -f docker-compose-prod.yml build --no-cache +``` + +2. Run all the services (in detached mode). + +``` +docker-compose -f docker-compose-prod.yml up -d +``` + +To stop all the services, use the following command: + +``` +docker-compose -f docker-compose-prod.yml down +``` + ## Useful links - User Service: http://localhost:3001 diff --git a/backend/code-execution-service/.dockerignore b/backend/code-execution-service/.dockerignore index 4abc77f632..d1d5485927 100644 --- a/backend/code-execution-service/.dockerignore +++ b/backend/code-execution-service/.dockerignore @@ -3,3 +3,4 @@ node_modules tests .env* *.md +dist diff --git a/backend/collab-service/.dockerignore b/backend/collab-service/.dockerignore index 4abc77f632..d1d5485927 100644 --- a/backend/collab-service/.dockerignore +++ b/backend/collab-service/.dockerignore @@ -3,3 +3,4 @@ node_modules tests .env* *.md +dist diff --git a/backend/communication-service/.dockerignore b/backend/communication-service/.dockerignore index 4abc77f632..d1d5485927 100644 --- a/backend/communication-service/.dockerignore +++ b/backend/communication-service/.dockerignore @@ -3,3 +3,4 @@ node_modules tests .env* *.md +dist diff --git a/backend/matching-service/.dockerignore b/backend/matching-service/.dockerignore index 4abc77f632..d1d5485927 100644 --- a/backend/matching-service/.dockerignore +++ b/backend/matching-service/.dockerignore @@ -3,3 +3,4 @@ node_modules tests .env* *.md +dist diff --git a/backend/qn-history-service/.dockerignore b/backend/qn-history-service/.dockerignore index 4abc77f632..d1d5485927 100644 --- a/backend/qn-history-service/.dockerignore +++ b/backend/qn-history-service/.dockerignore @@ -3,3 +3,4 @@ node_modules tests .env* *.md +dist diff --git a/backend/question-service/.dockerignore b/backend/question-service/.dockerignore index 4abc77f632..d1d5485927 100644 --- a/backend/question-service/.dockerignore +++ b/backend/question-service/.dockerignore @@ -3,3 +3,4 @@ node_modules tests .env* *.md +dist diff --git a/backend/user-service/.dockerignore b/backend/user-service/.dockerignore index 4abc77f632..d1d5485927 100644 --- a/backend/user-service/.dockerignore +++ b/backend/user-service/.dockerignore @@ -3,3 +3,4 @@ node_modules tests .env* *.md +dist diff --git a/frontend/.dockerignore b/frontend/.dockerignore index 9b07ce379e..3f2bee0ca7 100644 --- a/frontend/.dockerignore +++ b/frontend/.dockerignore @@ -1,3 +1,4 @@ coverage node_modules *.md +dist From 6561d2f78d7b22488f98eb6210692f692dcfdd71 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 13 Nov 2024 13:26:26 +0800 Subject: [PATCH 183/192] Fix matching service tests --- backend/matching-service/tests/webSocketHandler.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/matching-service/tests/webSocketHandler.spec.ts b/backend/matching-service/tests/webSocketHandler.spec.ts index a2833108da..e904ed08d4 100644 --- a/backend/matching-service/tests/webSocketHandler.spec.ts +++ b/backend/matching-service/tests/webSocketHandler.spec.ts @@ -3,7 +3,7 @@ import { type AddressInfo } from "node:net"; import ioc from "socket.io-client"; import { Server, Socket } from "socket.io"; import { MatchEvents } from "../src/handlers/websocketHandler"; -import { MatchUser } from "../src/handlers/matchHandler"; +import { MatchUser } from "../src/utils/types"; describe("Matching service web socket", () => { let io: Server, serverSocket: Socket, clientSocket: SocketIOClient.Socket; From 0d78b274d47dd7d4bc1228c4a1a4b15c20a61e76 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 13 Nov 2024 13:32:31 +0800 Subject: [PATCH 184/192] Fix frontend linting --- frontend/src/components/Chat/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 7fdcefba09..eefc7d1245 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -92,6 +92,7 @@ const Chat: React.FC = ({ isActive, setHasNewMessage }) => { communicationSocket.off(CommunicationEvents.DISCONNECTED, listener); communicationSocket.off(CommunicationEvents.CONNECT_ERROR, errorListener); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { From af35589bf0487664d32bd519f3178014020cc3fc Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 13 Nov 2024 13:37:32 +0800 Subject: [PATCH 185/192] Fix dockerfile warnings --- backend/user-service/Dockerfile | 2 +- frontend/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/user-service/Dockerfile b/backend/user-service/Dockerfile index 550b015a95..5f6957c64d 100644 --- a/backend/user-service/Dockerfile +++ b/backend/user-service/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine as base +FROM node:20-alpine AS base WORKDIR /user-service diff --git a/frontend/Dockerfile b/frontend/Dockerfile index a78b37563e..0a069a4194 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine as base +FROM node:20-alpine AS base WORKDIR /frontend From fe3e5b6f98a52d3e4c5fee3ec38a19ac280a84a0 Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 13 Nov 2024 14:24:23 +0800 Subject: [PATCH 186/192] Comment out build step --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6f20f924a..516d5977e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,8 @@ jobs: run: npm run lint - name: Test run: docker compose -f docker-compose-test.yml run --rm test-frontend - - name: Build - run: docker compose -f docker-compose-prod.yml build frontend + # - name: Build + # run: docker compose -f docker-compose-prod.yml build frontend backend-ci: runs-on: ubuntu-latest strategy: @@ -67,5 +67,5 @@ jobs: JWT_SECRET: ${{ secrets.JWT_SECRET }} ONE_COMPILER_KEY: ${{ secrets.ONE_COMPILER_KEY }} run: docker compose -f docker-compose-test.yml run --rm test-${{ matrix.service }} - - name: Build - run: docker compose -f docker-compose-prod.yml build ${{ matrix.service }} + # - name: Build + # run: docker compose -f docker-compose-prod.yml build ${{ matrix.service }} From 5c17ab70b7e442364c7f4f1ce2b33a316c002959 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:57:25 +0800 Subject: [PATCH 187/192] Add reference --- backend/question-service/src/scripts/README.md | 3 +-- backend/user-service/README.md | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/question-service/src/scripts/README.md b/backend/question-service/src/scripts/README.md index 4940ac4a73..1ea9b785ab 100644 --- a/backend/question-service/src/scripts/README.md +++ b/backend/question-service/src/scripts/README.md @@ -1,6 +1,5 @@ # Question Service Seed -Questions, test cases, code templates, and solutions are generated using ChatGPT. +Questions, test cases, code templates, and solutions are generated using [ChatGPT](https://chatgpt.com/share/672a353e-b66c-8006-807d-9e15c21fe520). Disclaimer: They may not be fully accurate. - diff --git a/backend/user-service/README.md b/backend/user-service/README.md index fb202181f7..861471e887 100644 --- a/backend/user-service/README.md +++ b/backend/user-service/README.md @@ -1,5 +1,7 @@ # User Service Guide +> This User Service is adapted from [PeerPrep-UserService](https://github.com/CS3219-AY2425S1/PeerPrep-UserService/tree/main/user-service). + > Please ensure that you have completed the backend set-up [here](../README.md) before proceeding. ## Setting-up User Service From f871392ce4b34ec4a84939ffbb428c04bd6431eb Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:14:12 +0800 Subject: [PATCH 188/192] Update test --- .../code-execution-service/tests/codeExecutionRoutes.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts index 81996aef95..3bbe293041 100644 --- a/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts +++ b/backend/code-execution-service/tests/codeExecutionRoutes.spec.ts @@ -1,5 +1,5 @@ import supertest from "supertest"; -import app from "../app"; +import app from "../src/app"; import { ERROR_MISSING_REQUIRED_FIELDS_MESSAGE, ERROR_UNSUPPORTED_LANGUAGE_MESSAGE, From 56518d4a63093d7cc3caff4803905fa2c1e14bef Mon Sep 17 00:00:00 2001 From: Nicole Lim Date: Wed, 13 Nov 2024 16:34:33 +0800 Subject: [PATCH 189/192] Update tsconfig --- backend/code-execution-service/package.json | 2 +- backend/code-execution-service/tsconfig.json | 12 ++++++------ backend/collab-service/package.json | 2 +- backend/collab-service/tsconfig.json | 12 ++++++------ backend/communication-service/package.json | 2 +- backend/communication-service/tsconfig.json | 12 ++++++------ backend/matching-service/package.json | 2 +- backend/matching-service/tsconfig.json | 12 ++++++------ backend/qn-history-service/package.json | 2 +- backend/qn-history-service/tsconfig.json | 12 ++++++------ backend/question-service/package.json | 2 +- backend/question-service/tsconfig.json | 12 ++++++------ backend/user-service/package.json | 2 +- backend/user-service/tsconfig.json | 8 ++++---- 14 files changed, 47 insertions(+), 47 deletions(-) diff --git a/backend/code-execution-service/package.json b/backend/code-execution-service/package.json index 29661dde80..567e9c7a41 100644 --- a/backend/code-execution-service/package.json +++ b/backend/code-execution-service/package.json @@ -4,7 +4,7 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx dist/server.js", + "start": "tsx dist/src/server.js", "dev": "tsx watch src/server.ts", "build": "tsc", "test": "cross-env NODE_ENV=test && jest", diff --git a/backend/code-execution-service/tsconfig.json b/backend/code-execution-service/tsconfig.json index ce40ce2979..ff1a6bc49b 100644 --- a/backend/code-execution-service/tsconfig.json +++ b/backend/code-execution-service/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, + "rootDir": "." /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -53,9 +53,9 @@ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ + // "noEmit": true /* Disable emitting files from a compilation. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ @@ -107,6 +107,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "include": ["**/**/*.ts"] } diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index 37e72d5e01..74da3baad0 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -4,7 +4,7 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx src/server.js", + "start": "tsx dist/src/server.js", "dev": "tsx watch src/server.ts", "build": "tsc", "test": "cross-env NODE_ENV=test && jest", diff --git a/backend/collab-service/tsconfig.json b/backend/collab-service/tsconfig.json index ce40ce2979..ff1a6bc49b 100644 --- a/backend/collab-service/tsconfig.json +++ b/backend/collab-service/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, + "rootDir": "." /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -53,9 +53,9 @@ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ + // "noEmit": true /* Disable emitting files from a compilation. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ @@ -107,6 +107,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "include": ["**/**/*.ts"] } diff --git a/backend/communication-service/package.json b/backend/communication-service/package.json index 322a5b23a4..65a5776359 100644 --- a/backend/communication-service/package.json +++ b/backend/communication-service/package.json @@ -4,7 +4,7 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx src/server.js", + "start": "tsx dist/src/server.js", "dev": "tsx watch src/server.ts", "build": "tsc", "test": "cross-env NODE_ENV=test && jest", diff --git a/backend/communication-service/tsconfig.json b/backend/communication-service/tsconfig.json index ce40ce2979..ff1a6bc49b 100644 --- a/backend/communication-service/tsconfig.json +++ b/backend/communication-service/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, + "rootDir": "." /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -53,9 +53,9 @@ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ + // "noEmit": true /* Disable emitting files from a compilation. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ @@ -107,6 +107,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "include": ["**/**/*.ts"] } diff --git a/backend/matching-service/package.json b/backend/matching-service/package.json index 9c3fffcb2d..0ac3995c24 100644 --- a/backend/matching-service/package.json +++ b/backend/matching-service/package.json @@ -4,7 +4,7 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx dist/server.js", + "start": "tsx dist/src/server.js", "dev": "tsx watch src/server.ts", "build": "tsc", "test": "cross-env NODE_ENV=test && jest", diff --git a/backend/matching-service/tsconfig.json b/backend/matching-service/tsconfig.json index ce40ce2979..ff1a6bc49b 100644 --- a/backend/matching-service/tsconfig.json +++ b/backend/matching-service/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, + "rootDir": "." /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -53,9 +53,9 @@ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ + // "noEmit": true /* Disable emitting files from a compilation. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ @@ -107,6 +107,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "include": ["**/**/*.ts"] } diff --git a/backend/qn-history-service/package.json b/backend/qn-history-service/package.json index ce050aa20b..428a328e78 100644 --- a/backend/qn-history-service/package.json +++ b/backend/qn-history-service/package.json @@ -4,7 +4,7 @@ "main": "server.ts", "type": "module", "scripts": { - "start": "tsx dist/server.js", + "start": "tsx dist/src/server.js", "dev": "tsx watch src/server.ts", "build": "tsc", "test": "cross-env NODE_ENV=test && jest", diff --git a/backend/qn-history-service/tsconfig.json b/backend/qn-history-service/tsconfig.json index ce40ce2979..ff1a6bc49b 100644 --- a/backend/qn-history-service/tsconfig.json +++ b/backend/qn-history-service/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, + "rootDir": "." /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -53,9 +53,9 @@ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ + // "noEmit": true /* Disable emitting files from a compilation. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "./dist" /* Specify an output folder for all emitted files. */, // "removeComments": true, /* Disable emitting comments. */ @@ -107,6 +107,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "include": ["**/**/*.ts"] } diff --git a/backend/question-service/package.json b/backend/question-service/package.json index 2733cd2ece..4a2a921c34 100644 --- a/backend/question-service/package.json +++ b/backend/question-service/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "seed": "tsx src/scripts/seed.ts", - "start": "tsx dist/server.js", + "start": "tsx dist/src/server.js", "build": "tsc", "dev": "tsx watch src/server.ts", "test": "cross-env NODE_ENV=test && jest", diff --git a/backend/question-service/tsconfig.json b/backend/question-service/tsconfig.json index b7501d7a18..ff1a6bc49b 100644 --- a/backend/question-service/tsconfig.json +++ b/backend/question-service/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, + "rootDir": "." /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -40,7 +40,7 @@ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - "resolveJsonModule": true /* Enable importing .json files. */, + // "resolveJsonModule": true, /* Enable importing .json files. */ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ @@ -53,7 +53,7 @@ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "noEmit": true /* Disable emitting files from a compilation. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ @@ -107,6 +107,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "dist"], + "include": ["**/**/*.ts"] } diff --git a/backend/user-service/package.json b/backend/user-service/package.json index aef24b7c49..8135452e0a 100644 --- a/backend/user-service/package.json +++ b/backend/user-service/package.json @@ -5,7 +5,7 @@ "main": "app.ts", "type": "module", "scripts": { - "start": "tsx dist/server.js", + "start": "tsx dist/src/server.js", "dev": "tsx watch src/server.ts", "build": "tsc", "lint": "eslint .", diff --git a/backend/user-service/tsconfig.json b/backend/user-service/tsconfig.json index 972bb166b1..ff1a6bc49b 100644 --- a/backend/user-service/tsconfig.json +++ b/backend/user-service/tsconfig.json @@ -26,7 +26,7 @@ /* Modules */ "module": "ESNext" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, + "rootDir": "." /* Specify the root folder within your source files. */, "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ @@ -53,7 +53,7 @@ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true /* Create source map files for emitted JavaScript files. */, + // "sourceMap": true /* Create source map files for emitted JavaScript files. */, // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "noEmit": true /* Disable emitting files from a compilation. */, // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ @@ -107,6 +107,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "exclude": ["node_modules"], - "include": ["src/**/*"] + "exclude": ["node_modules", "dist"], + "include": ["**/**/*.ts"] } From 7d2f7fe26c3810c115b56d028495e6bf571079fd Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:53:20 +0800 Subject: [PATCH 190/192] Fix test --- .../src/components/Navbar/Navbar.test.tsx | 25 ++++++++++++++ frontend/src/utils/api.ts | 33 ++++++++++++++----- frontend/src/utils/collabSocket.ts | 7 ++-- frontend/src/utils/communicationSocket.ts | 7 ++-- frontend/src/utils/matchSocket.ts | 7 ++-- 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/Navbar/Navbar.test.tsx b/frontend/src/components/Navbar/Navbar.test.tsx index 454b1d0315..c556bf53bb 100644 --- a/frontend/src/components/Navbar/Navbar.test.tsx +++ b/frontend/src/components/Navbar/Navbar.test.tsx @@ -8,6 +8,31 @@ import { MemoryRouter } from "react-router-dom"; jest.mock("axios"); +jest.mock("../../utils/api", () => ({ + getUserUrl: jest.fn().mockReturnValue("http://localhost:3001/api"), + getQuestionsUrl: jest + .fn() + .mockReturnValue("http://localhost:3000/api/questions"), + getCodeExecutionUrl: jest + .fn() + .mockReturnValue("http://localhost:3004/api/run"), + getQnHistoriesUrl: jest + .fn() + .mockReturnValue("http://localhost:3006/api/qnhistories"), +})); + +jest.mock("../../utils/matchSocket", () => ({ + getMatchSocketUrl: jest.fn().mockReturnValue("http://localhost:3002"), +})); + +jest.mock("../../utils/collabSocket", () => ({ + getCollabSocketUrl: jest.fn().mockReturnValue("http://localhost:3003"), +})); + +jest.mock("../../utils/communicationSocket", () => ({ + getCommunicationSocketUrl: jest.fn().mockReturnValue("http://localhost:3005"), +})); + const mockedAxios = axios as jest.Mocked; const mockUseNavigate = jest.fn(); diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index 410f668de7..73322406d8 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -1,14 +1,29 @@ import axios from "axios"; -const usersUrl = - import.meta.env.VITE_USER_SERVICE_URL ?? "http://localhost:3001/api"; -const questionsUrl = - import.meta.env.VITE_QN_SERVICE_URL ?? "http://localhost:3000/api/questions"; -const codeExecutionUrl = - import.meta.env.VITE_CODE_EXEC_SERVICE_URL ?? "http://localhost:3004/api/run"; -const qnHistoriesUrl = - import.meta.env.VITE_QN_HIST_SERVICE_URL ?? - "http://localhost:3006/api/qnhistories"; +const getUserUrl = () => { + return import.meta.env.SOME_ENV_VAR_HERE ?? "http://localhost:3001/api"; +}; + +const getQuestionsUrl = () => { + return ( + import.meta.env.SOME_ENV_VAR_HERE ?? "http://localhost:3000/api/questions" + ); +}; + +const getCodeExecutionUrl = () => { + return import.meta.env.SOME_ENV_VAR_HERE ?? "http://localhost:3004/api/run"; +}; + +const getQnHistoriesUrl = () => { + return ( + import.meta.env.SOME_ENV_VAR_HERE ?? "http://localhost:3006/api/qnhistories" + ); +}; + +const usersUrl = getUserUrl(); +const questionsUrl = getQuestionsUrl(); +const codeExecutionUrl = getCodeExecutionUrl(); +const qnHistoriesUrl = getQnHistoriesUrl(); export const questionClient = axios.create({ baseURL: questionsUrl, diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts index 7d6461c201..3c1be72be7 100644 --- a/frontend/src/utils/collabSocket.ts +++ b/frontend/src/utils/collabSocket.ts @@ -38,8 +38,11 @@ export type CollabSessionData = { awareness: Awareness; }; -const COLLAB_SOCKET_URL = - import.meta.env.VITE_COLLAB_SERVICE_URL ?? "http://localhost:3003"; +const getCollabSocketUrl = () => { + return import.meta.env.VITE_COLLAB_SERVICE_URL ?? "http://localhost:3003"; +}; + +const COLLAB_SOCKET_URL = getCollabSocketUrl(); export const collabSocket = io(COLLAB_SOCKET_URL, { reconnectionAttempts: 5, diff --git a/frontend/src/utils/communicationSocket.ts b/frontend/src/utils/communicationSocket.ts index 8356102b48..395ad6541a 100644 --- a/frontend/src/utils/communicationSocket.ts +++ b/frontend/src/utils/communicationSocket.ts @@ -16,8 +16,11 @@ export enum CommunicationEvents { DISCONNECTED = "disconnected", } -const COMMUNICATION_SOCKET_URL = - import.meta.env.VITE_COMM_SERVICE_URL ?? "http://localhost:3005"; +const getCommunicationSocketUrl = () => { + return import.meta.env.VITE_COMM_SERVICE_URL ?? "http://localhost:3005"; +}; + +const COMMUNICATION_SOCKET_URL = getCommunicationSocketUrl(); export const communicationSocket = io(COMMUNICATION_SOCKET_URL, { reconnectionAttempts: 3, diff --git a/frontend/src/utils/matchSocket.ts b/frontend/src/utils/matchSocket.ts index 754c96157c..0951e6e877 100644 --- a/frontend/src/utils/matchSocket.ts +++ b/frontend/src/utils/matchSocket.ts @@ -1,8 +1,11 @@ import { io } from "socket.io-client"; import { getToken } from "./token"; -const MATCH_SOCKET_URL = - import.meta.env.VITE_MATCH_SERVICE_URL ?? "http://localhost:3002"; +const getMatchSocketUrl = () => { + return import.meta.env.VITE_MATCH_SERVICE_URL ?? "http://localhost:3002"; +}; + +const MATCH_SOCKET_URL = getMatchSocketUrl(); export const matchSocket = io(MATCH_SOCKET_URL, { reconnectionAttempts: 3, From 30db5ca66b0d7b8cbf27b8c2bedd73f725311f08 Mon Sep 17 00:00:00 2001 From: Guan Quan <76832850+guanquann@users.noreply.github.com> Date: Wed, 13 Nov 2024 17:07:40 +0800 Subject: [PATCH 191/192] Update api --- frontend/src/utils/api.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index 73322406d8..f00e249098 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -1,22 +1,26 @@ import axios from "axios"; const getUserUrl = () => { - return import.meta.env.SOME_ENV_VAR_HERE ?? "http://localhost:3001/api"; + return import.meta.env.VITE_USER_SERVICE_URL ?? "http://localhost:3001/api"; }; const getQuestionsUrl = () => { return ( - import.meta.env.SOME_ENV_VAR_HERE ?? "http://localhost:3000/api/questions" + import.meta.env.VITE_QN_SERVICE_URL ?? "http://localhost:3000/api/questions" ); }; const getCodeExecutionUrl = () => { - return import.meta.env.SOME_ENV_VAR_HERE ?? "http://localhost:3004/api/run"; + return ( + import.meta.env.VITE_CODE_EXEC_SERVICE_URL ?? + "http://localhost:3004/api/run" + ); }; const getQnHistoriesUrl = () => { return ( - import.meta.env.SOME_ENV_VAR_HERE ?? "http://localhost:3006/api/qnhistories" + import.meta.env.VITE_QN_HIST_SERVICE_URL ?? + "http://localhost:3006/api/qnhistories" ); }; From 1d8e71a76937669af8431145de40802d15d3b0fb Mon Sep 17 00:00:00 2001 From: jolynloh Date: Thu, 14 Nov 2024 00:24:24 +0800 Subject: [PATCH 192/192] Modify README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4575367929..1756a81285 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # CS3219 Project (PeerPrep) - AY2425S1 Group 28 +## Deployment + +We deployed PeerPrep on AWS ECS. It is accessible [here](http://peerprep-frontend-alb-1935920115.ap-southeast-1.elb.amazonaws.com/). + ## Setting up We will be using Docker to set up PeerPrep. Install Docker [here](https://docs.docker.com/get-started/get-docker).