From 47525a5e21eea4109cb3edc515245ce4abde030b Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Fri, 9 Jan 2026 16:24:13 +0100 Subject: [PATCH] [#451] added websocket connexion --- package-lock.json | 119 ++++++++++-------- public/.env.example.js | 1 + src/App.tsx | 2 + src/websocket/WebSocketGate.tsx | 67 ++++++++++ src/websocket/api/fetchWebSocketTicket.ts | 12 ++ src/websocket/api/types.ts | 7 ++ src/websocket/createWebSocketConnection.ts | 63 ++++++++++ src/websocket/protocols.ts | 13 ++ .../websocketAPI/registerToCalendars.ts | 17 +++ .../websocketAPI/unregisterToCalendars.ts | 18 +++ 10 files changed, 270 insertions(+), 49 deletions(-) create mode 100644 src/websocket/WebSocketGate.tsx create mode 100644 src/websocket/api/fetchWebSocketTicket.ts create mode 100644 src/websocket/api/types.ts create mode 100644 src/websocket/createWebSocketConnection.ts create mode 100644 src/websocket/protocols.ts create mode 100644 src/websocket/websocketAPI/registerToCalendars.ts create mode 100644 src/websocket/websocketAPI/unregisterToCalendars.ts diff --git a/package-lock.json b/package-lock.json index 4380626..4bacb70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,38 +79,6 @@ "node": "24.x" } }, - "../twake-ui/packages/twake-mui": { - "name": "@linagora/twake-mui", - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^18.3.26", - "@types/react-dom": "^18.3.7", - "typescript": "^4.9.5" - }, - "engines": { - "node": "24.x" - }, - "peerDependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/material": "^7.1.2", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": false - }, - "react-dom": { - "optional": false - } - } - }, "node_modules/@adobe/css-tools": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", @@ -169,6 +137,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2101,6 +2070,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2124,6 +2094,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2226,6 +2197,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2269,6 +2241,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2541,6 +2514,7 @@ "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", "license": "MIT", + "peer": true, "dependencies": { "preact": "~10.12.1" } @@ -3164,8 +3138,24 @@ } }, "node_modules/@linagora/twake-mui": { - "resolved": "../twake-ui/packages/twake-mui", - "link": true + "version": "1.1.1", + "resolved": "https://npm.pkg.github.com/download/@linagora/twake-mui/1.1.1/16fcea760c76c020860b19640c010ddc4c1ba9f3", + "integrity": "sha512-3MCOnhiwUKUI4WH/Mrx6R9cyyCJZoIKbc6TUoTQew8s4PprhvXI+xElOOVgBsCdCkZ9DOpeOOrcvMzDbd9JYNg==", + "license": "MIT", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "engines": { + "node": "24.x" + }, + "peerDependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^7.1.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } }, "node_modules/@module-federation/error-codes": { "version": "0.21.6", @@ -3267,6 +3257,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.6", @@ -3377,6 +3368,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.6.tgz", "integrity": "sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.6", @@ -3721,9 +3713,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", - "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -3735,6 +3727,7 @@ "integrity": "sha512-LvoOF53PL6zXgdzEhgnnP51S4FseDFH1bHrobK4EK6zZX/tN8qgf5tdlmN7h4OkMv/Qs1oUfvj0QcLWSstnnvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rspack/core": "1.6.8", "@rspack/lite-tapable": "~1.1.0", @@ -3962,6 +3955,7 @@ "integrity": "sha512-FolcIAH5FW4J2FET+qwjd1kNeFbCkd0VLuIHO0thyolEjaPSxw5qxG67DA7BZGm6PVcoiSgPLks1DL6eZ8c+fA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.21.6", "@rspack/binding": "1.6.8", @@ -4221,6 +4215,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -4305,6 +4300,7 @@ "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -4315,6 +4311,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4610,6 +4607,7 @@ "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4631,6 +4629,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4642,6 +4641,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4698,6 +4698,7 @@ "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", @@ -4727,6 +4728,7 @@ "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", @@ -5233,6 +5235,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5992,6 +5995,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6703,6 +6707,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -6718,7 +6723,8 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -7317,6 +7323,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8671,6 +8678,7 @@ "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.7.6" } @@ -9645,6 +9653,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10408,6 +10417,7 @@ "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.19.6", "@svgr/babel-preset": "^6.5.1", @@ -10866,6 +10876,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -11302,6 +11313,7 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -11311,6 +11323,7 @@ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", "license": "MIT", + "peer": true, "dependencies": { "moment": "^2.29.4" }, @@ -11346,7 +11359,6 @@ } ], "license": "MIT", - "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -12283,6 +12295,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12305,6 +12318,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12324,6 +12338,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -12348,17 +12363,18 @@ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-router": { - "version": "6.30.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", - "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.1" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -12368,13 +12384,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", - "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.1", - "react-router": "6.30.2" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -12448,7 +12464,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-first-history": { "version": "5.2.0", @@ -13675,6 +13692,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13855,6 +13873,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14071,6 +14090,7 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14879,6 +14899,7 @@ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/public/.env.example.js b/public/.env.example.js index 0ce6d30..405104b 100644 --- a/public/.env.example.js +++ b/public/.env.example.js @@ -10,3 +10,4 @@ var MAIL_SPA_URL = "https://mail.example.com"; var VIDEO_CONFERENCE_BASE_URL = "https://meet.linagora.com"; var DEBUG = false; var LANG = "en"; +var WEBSOCKET_URL = "wss://calendar.example.com"; diff --git a/src/App.tsx b/src/App.tsx index 4ba7509..ad8be03 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import fr from "./locales/fr.json"; import ru from "./locales/ru.json"; import vi from "./locales/vi.json"; import I18n from "twake-i18n"; +import { WebSocketGate } from "./websocket/WebSocketGate"; const locale = { en, fr, ru, vi }; const dateLocales = { en: enGB, fr: frLocale, ru: ruLocale, vi: viLocale }; @@ -68,6 +69,7 @@ function App() { locales={dateLocales} > }> + } /> diff --git a/src/websocket/WebSocketGate.tsx b/src/websocket/WebSocketGate.tsx new file mode 100644 index 0000000..d49c964 --- /dev/null +++ b/src/websocket/WebSocketGate.tsx @@ -0,0 +1,67 @@ +import { useEffect, useRef } from "react"; +import { useAppSelector } from "../app/hooks"; +import { createWebSocketConnection } from "./createWebSocketConnection"; +import { registerToCalendars } from "./websocketAPI/registerToCalendars"; + +export function WebSocketGate() { + const socketRef = useRef(null); + + const isAuthenticated = useAppSelector((state) => + Boolean(state.user.userData && state.user.tokens) + ); + + const calendarList = useAppSelector((state) => state.calendars.list); + + // Manage WebSocket connection + useEffect(() => { + if (!isAuthenticated) { + if (socketRef.current) { + socketRef.current.close(); + socketRef.current = null; + } + return; + } + + const connect = async () => { + try { + const socket = await createWebSocketConnection(); + socketRef.current = socket; + } catch (error) { + console.error("Failed to create WebSocket connection:", error); + } + }; + + connect(); + + return () => { + if (socketRef.current) { + socketRef.current.close(); + socketRef.current = null; + } + }; + }, [isAuthenticated]); + + const hasRegisteredRef = useRef(false); + + // Register to calendars once + useEffect(() => { + if (!socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) { + return; + } + + if (hasRegisteredRef.current) { + return; + } + + const calendarPaths = Object.values(calendarList).map( + (cal) => `/calendars/${cal.id}` + ); + + if (calendarPaths.length > 0) { + registerToCalendars(socketRef.current, calendarPaths); + hasRegisteredRef.current = true; + } + }, [calendarList]); + + return null; +} diff --git a/src/websocket/api/fetchWebSocketTicket.ts b/src/websocket/api/fetchWebSocketTicket.ts new file mode 100644 index 0000000..05c6505 --- /dev/null +++ b/src/websocket/api/fetchWebSocketTicket.ts @@ -0,0 +1,12 @@ +import { api } from "../../utils/apiUtils"; +import { WebSocketTicket } from "./types"; + +export async function fetchWebSocketTicket(): Promise { + const response = await api.post("ws/ticket"); + + if (!response.ok) { + throw new Error("Failed to fetch WebSocket ticket"); + } + + return response.json(); +} diff --git a/src/websocket/api/types.ts b/src/websocket/api/types.ts new file mode 100644 index 0000000..cf1da08 --- /dev/null +++ b/src/websocket/api/types.ts @@ -0,0 +1,7 @@ +export interface WebSocketTicket { + clientAddress: string; + value: string; + generatedOn: string; + validUntil: string; + username: string; +} diff --git a/src/websocket/createWebSocketConnection.ts b/src/websocket/createWebSocketConnection.ts new file mode 100644 index 0000000..1e9aa8b --- /dev/null +++ b/src/websocket/createWebSocketConnection.ts @@ -0,0 +1,63 @@ +import { fetchWebSocketTicket } from "./api/fetchWebSocketTicket"; +import { WS_INBOUND_EVENTS } from "./protocols"; + +export async function createWebSocketConnection(): Promise { + const wsBaseUrl = (window as any).WEBSOCKET_URL; + + if (!wsBaseUrl) { + throw new Error("WEBSOCKET_URL is not defined"); + } + + const ticket = await fetchWebSocketTicket(); + + const socket = new WebSocket( + `${wsBaseUrl}/ws?ticket=${encodeURIComponent(ticket.value)}` + ); + + await new Promise((resolve, reject) => { + const openHandler = () => { + console.log("WebSocket connection opened"); + socket.removeEventListener( + WS_INBOUND_EVENTS.CONNECTION_OPENED, + openHandler + ); + socket.removeEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler); + resolve(); + }; + + const errorHandler = (error: Event) => { + console.error("WebSocket connection failed:", error); + socket.removeEventListener( + WS_INBOUND_EVENTS.CONNECTION_OPENED, + openHandler + ); + socket.removeEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler); + reject(new Error("WebSocket connection failed")); + }; + + socket.addEventListener(WS_INBOUND_EVENTS.CONNECTION_OPENED, openHandler); + socket.addEventListener(WS_INBOUND_EVENTS.ERROR, errorHandler); + }); + + socket.addEventListener(WS_INBOUND_EVENTS.MESSAGE, (event) => { + try { + const message = JSON.parse(event.data); + console.log("WebSocket message received:", message); + + // TODO: Handle different message types + } catch (error) { + console.error("Failed to parse WebSocket message:", error); + } + }); + + socket.addEventListener(WS_INBOUND_EVENTS.CONNECTION_CLOSED, (event) => { + console.log("WebSocket closed:", event.code, event.reason); + // TODO: Add reconnection logic + }); + + socket.addEventListener(WS_INBOUND_EVENTS.ERROR, (error) => { + console.error("WebSocket error:", error); + }); + + return socket; +} diff --git a/src/websocket/protocols.ts b/src/websocket/protocols.ts new file mode 100644 index 0000000..89eacff --- /dev/null +++ b/src/websocket/protocols.ts @@ -0,0 +1,13 @@ +// WebSocket event listeners (browser events) +export const WS_INBOUND_EVENTS = { + CONNECTION_OPENED: "open", + MESSAGE: "message", + ERROR: "error", + CONNECTION_CLOSED: "close", +} as const; + +// WebSocket message types sent to server +export const WS_OUTBOUND_EVENTS = { + REGISTER_CLIENT: "register", + UNREGISTER_CLIENT: "unregister", +} as const; diff --git a/src/websocket/websocketAPI/registerToCalendars.ts b/src/websocket/websocketAPI/registerToCalendars.ts new file mode 100644 index 0000000..47665e1 --- /dev/null +++ b/src/websocket/websocketAPI/registerToCalendars.ts @@ -0,0 +1,17 @@ +export function registerToCalendars( + socket: WebSocket, + calendarURIList: string[] +): void { + if (socket.readyState !== WebSocket.OPEN) { + throw new Error("Cannot register: WebSocket is not open"); + } + []; + + socket.send( + JSON.stringify({ + register: calendarURIList, + }) + ); + + console.log("Registered to calendars", calendarURIList); +} diff --git a/src/websocket/websocketAPI/unregisterToCalendars.ts b/src/websocket/websocketAPI/unregisterToCalendars.ts new file mode 100644 index 0000000..4700a60 --- /dev/null +++ b/src/websocket/websocketAPI/unregisterToCalendars.ts @@ -0,0 +1,18 @@ +export function unregisterToCalendars( + socket: WebSocket, + calendarURIList: string[] +): void { + if (socket.readyState !== WebSocket.OPEN) { + throw new Error("Cannot unregister: WebSocket is not open"); + } + []; + console.log(calendarURIList); + + socket.send( + JSON.stringify({ + unregister: calendarURIList, + }) + ); + + console.log("Unregistered to calendars", calendarURIList); +}