From 465a5ffdd38d0e322ee92799dfc0c1de2bbecd82 Mon Sep 17 00:00:00 2001 From: Nicholas Piano Date: Thu, 27 Apr 2023 13:13:30 +0200 Subject: [PATCH 1/4] feat: add caching step for each message --- package.json | 3 ++- src/service/socket.service.js | 30 ++++++++++++++++++++++++++---- yarn.lock | 5 +++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 17a201c..08958fb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@heroicons/vue": "^1.0.5", "axios": "^0.24.0", "socket.io-client": "^4.4.0", + "uuid": "^9.0.0", "vue": "^3.2.16", "vue-cookie-next": "^1.3.0", "vue-router": "4" @@ -25,4 +26,4 @@ "tailwindcss": "^2.2.19", "vite": "^2.6.4" } -} \ No newline at end of file +} diff --git a/src/service/socket.service.js b/src/service/socket.service.js index e65e36b..b77b615 100644 --- a/src/service/socket.service.js +++ b/src/service/socket.service.js @@ -1,8 +1,10 @@ +import { v4 as uuidv4 } from 'uuid'; import { io, Socket } from "socket.io-client"; /** * Singleton Socket Service class */ class SocketioService { + _requestCache = {}; _socketInstance = null; constructor() { if (!this._socketInstance) { @@ -28,6 +30,9 @@ class SocketioService { ); }; setupSocketConnection(); + + // set up cache + this._requestCache = {}; } } @@ -39,10 +44,22 @@ class SocketioService { this._socketInstance.on("disconnect", onDisconnectHandler); } - addMessageHandler(onMessageHandler) { + addMessageHandler(finalOnMessageHandler) { + const onMessageHandler = (data) => { + if (data.request) { + const { resolve, reject } = this._requestCache[data.request]; + if (data.error) { + reject(data.error); + } else { + resolve(finalOnMessageHandler(data)); + } + delete this._requestCache[request]; + } + }; + // New message bind this._socketInstance.off("message:new", onMessageHandler); - this._socketInstance.on("message:new", onMessageHandler); + this._socketInstance.on("message:new", onMessageHandler); } addThreadHandler(onThreadHandler) { @@ -80,8 +97,13 @@ class SocketioService { } } - sendMessage(payload) { - this._socketInstance.emit("message:create", payload); + async sendMessage(payload) { + const request = uuidv4(); + + return new Promise((resolve, reject) => { + this._requestCache[request] = { resolve, reject, payload }; + this._socketInstance.emit("message:create", { ...payload, request }); + }); } joinThread(payload) { diff --git a/yarn.lock b/yarn.lock index 49f06ed..248d40e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1681,6 +1681,11 @@ util-deprecate@^1.0.2: resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" From ca011984a1c4bcbc8f3de9d77b64668479ddf540 Mon Sep 17 00:00:00 2001 From: Nicholas Piano Date: Mon, 1 May 2023 16:54:37 +0200 Subject: [PATCH 2/4] feat: display message on error --- src/service/socket.service.js | 2 +- src/views/MessagesPage.vue | 32 ++++++++++++++++++++++---------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/service/socket.service.js b/src/service/socket.service.js index b77b615..9e3ecaa 100644 --- a/src/service/socket.service.js +++ b/src/service/socket.service.js @@ -53,7 +53,7 @@ class SocketioService { } else { resolve(finalOnMessageHandler(data)); } - delete this._requestCache[request]; + delete this._requestCache[data.request]; } }; diff --git a/src/views/MessagesPage.vue b/src/views/MessagesPage.vue index d4341d6..e472f2e 100644 --- a/src/views/MessagesPage.vue +++ b/src/views/MessagesPage.vue @@ -51,6 +51,12 @@ This thread is now locked. Messages cannot be sent until it is unlocked by the thread creator. + + Unable to send message. Please try again later. + @@ -116,6 +122,7 @@ const searchTag = ref(""); const goodReputation = ref(false); const wsInstance = reactive({}); const shouldDisplayMessageBoxLocked = ref(false); +const shouldDisplayUnableToSendMessage = ref(false); /** * Dialog feature @@ -236,16 +243,21 @@ const updatedMsgs = computed((x) => { */ async function sendMessage() { if (message.value.trim().length > 0 && !getActiveThread.value?.locked && !pseudonymMismatch.value) { - wsInstance.value.sendMessage({ - message: { - body: message.value, - thread: route.params.threadId, - }, - token: VueCookieNext.getCookie("access_token"), - }); - - message.value = ""; - scrollToBottom(); + try { + await wsInstance.value.sendMessage({ + message: { + body: message.value, + thread: route.params.threadId, + }, + token: VueCookieNext.getCookie("access_token"), + }); + + shouldDisplayUnableToSendMessage.value = false; + message.value = ""; + scrollToBottom(); + } catch (error) { + shouldDisplayUnableToSendMessage.value = true; + } } } From a4f9c67fbce48317bfdcb826bb96faa48d0e9daa Mon Sep 17 00:00:00 2001 From: Nicholas Piano Date: Mon, 8 May 2023 19:58:29 +0200 Subject: [PATCH 3/4] fix: correctly process errors and allow websocket refresh token --- src/components/Threads/EditThread.vue | 4 +- src/components/Threads/ThreadListItem.vue | 2 +- src/plugins/axios.js | 38 ++++++------- src/service/socket.service.js | 67 +++++++++++++++++------ src/views/MessagesPage.vue | 22 +++++++- 5 files changed, 94 insertions(+), 39 deletions(-) diff --git a/src/components/Threads/EditThread.vue b/src/components/Threads/EditThread.vue index 8680e51..09629f1 100644 --- a/src/components/Threads/EditThread.vue +++ b/src/components/Threads/EditThread.vue @@ -4,6 +4,7 @@ @click.prevent="openModal" title="Edit Thread" class="border-2 border-gray-500 h-10 w-12 mt-4 ml-2 hover:bg-gray-200" + data-testid="edit-thread" >
{{ message }}
diff --git a/src/components/Threads/ThreadListItem.vue b/src/components/Threads/ThreadListItem.vue index a176278..e1032a5 100644 --- a/src/components/Threads/ThreadListItem.vue +++ b/src/components/Threads/ThreadListItem.vue @@ -13,7 +13,7 @@ class="h-5 w-5 inline-block" /> -
+
diff --git a/src/plugins/axios.js b/src/plugins/axios.js index b2e32fb..b457cf9 100644 --- a/src/plugins/axios.js +++ b/src/plugins/axios.js @@ -5,29 +5,29 @@ axios.defaults.headers.common[ "Authorization" ] = `Bearer ${VueCookieNext.getCookie("access_token")}`; -axios.interceptors.response.use( - (response) => response, - (error) => { - const status = error.response ? error.response.status : null; +export function refreshToken() { + const refreshingCall = axios + .post(`${import.meta.env.VITE_API_SERVER_URL}/v1/auth/refresh-tokens`, { + refreshToken: VueCookieNext.getCookie("refresh_token"), + }) + .then((res) => { + VueCookieNext.setCookie("access_token", res.data.access.token); + VueCookieNext.setCookie("refresh_token", res.data.refresh.token); - function refreshToken() { - const refreshingCall = axios - .post(`${import.meta.env.VITE_API_SERVER_URL}/v1/auth/refresh-tokens`, { - refreshToken: VueCookieNext.getCookie("refresh_token"), - }) - .then((res) => { - VueCookieNext.setCookie("access_token", res.data.access.token); - VueCookieNext.setCookie("refresh_token", res.data.refresh.token); + axios.defaults.headers.common[ + "Authorization" + ] = `Bearer ${res.data.access.token}`; - axios.defaults.headers.common[ - "Authorization" - ] = `Bearer ${res.data.access.token}`; + return Promise.resolve(true); + }); - return Promise.resolve(true); - }); + return refreshingCall; +} - return refreshingCall; - } +axios.interceptors.response.use( + (response) => response, + (error) => { + const status = error.response ? error.response.status : null; if ( status === 401 && diff --git a/src/service/socket.service.js b/src/service/socket.service.js index 9e3ecaa..627507d 100644 --- a/src/service/socket.service.js +++ b/src/service/socket.service.js @@ -1,5 +1,8 @@ import { v4 as uuidv4 } from 'uuid'; import { io, Socket } from "socket.io-client"; +import { VueCookieNext } from "vue-cookie-next"; +import { refreshToken } from '../plugins/axios'; + /** * Singleton Socket Service class */ @@ -46,14 +49,12 @@ class SocketioService { addMessageHandler(finalOnMessageHandler) { const onMessageHandler = (data) => { - if (data.request) { - const { resolve, reject } = this._requestCache[data.request]; - if (data.error) { - reject(data.error); - } else { - resolve(finalOnMessageHandler(data)); - } + if (data.request && data.request in this._requestCache) { + const { resolve } = this._requestCache[data.request]; + resolve(finalOnMessageHandler(data)); delete this._requestCache[data.request]; + } else { + finalOnMessageHandler(data); } }; @@ -80,6 +81,41 @@ class SocketioService { this._socketInstance.on("vote:new", onVoteHandler); } + async sendMessage(payload) { + const request = uuidv4(); + + return new Promise((resolve, reject) => { + this._requestCache[request] = { resolve, reject, payload }; + this._socketInstance.emit("message:create", { ...payload, request }); + }); + } + + addErrorHandler() { + const onErrorHandler = async (data) => { + if (data.request) { + const { reject, resolve, payload } = this._requestCache[data.request]; + + if (data.error === "jwt expired") { + await refreshToken(); + await this.sendMessage({ + ...payload, + token: VueCookieNext.getCookie("access_token"), + }); + + resolve(); + } else { + reject(data.error); + } + + delete this._requestCache[data.request]; + } + }; + + // New error bind + this._socketInstance.off("error", onErrorHandler); + this._socketInstance.on("error", onErrorHandler); + } + disconnectTopic() { this._socketInstance.emit("topic:disconnect"); this.disconnect(); @@ -97,15 +133,6 @@ class SocketioService { } } - async sendMessage(payload) { - const request = uuidv4(); - - return new Promise((resolve, reject) => { - this._requestCache[request] = { resolve, reject, payload }; - this._socketInstance.emit("message:create", { ...payload, request }); - }); - } - joinThread(payload) { this._socketInstance.emit("thread:join", payload); } @@ -113,6 +140,14 @@ class SocketioService { joinTopic(payload) { this._socketInstance.emit("topic:join", payload); } + + joinUser(payload) { + this._socketInstance.emit("user:join", payload); + } + + onConnect(onConnectHandler) { + this._socketInstance.on("connect", onConnectHandler); + } } export default SocketioService; diff --git a/src/views/MessagesPage.vue b/src/views/MessagesPage.vue index e472f2e..50cab66 100644 --- a/src/views/MessagesPage.vue +++ b/src/views/MessagesPage.vue @@ -44,8 +44,9 @@ @keypress="watchTagging" @keydown.enter.prevent="sendMessage" class="w-full block border-2 text-sm px-1 h-20 mt-4" - :class="shouldDisplayMessageBoxLocked ? 'border-red-500' : 'border-gray-500'" + :class="(shouldDisplayMessageBoxLocked || shouldDisplayUnableToSendMessage) ? 'border-red-500' : 'border-gray-500'" placeholder="Message (hit enter to send)" + data-testid="message-text-area" > { + wsInstance.value.addErrorHandler(); wsInstance.value.addVotesHandler(onVoteHandler); wsInstance.value.addMessageHandler(messageHandler); - joinThread(route.params.threadId); + + wsInstance.value.onConnect(() => { + setTimeout(() => { + joinThread(route.params.threadId); + joinUser(); + }, 100); + }); } onMounted(async () => { From aa0a0b8b6b0f40b6bd41e06544ae4d111923b61a Mon Sep 17 00:00:00 2001 From: Nicholas Piano Date: Tue, 9 May 2023 11:25:25 +0200 Subject: [PATCH 4/4] fix: prevent request from repeating --- src/service/socket.service.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/service/socket.service.js b/src/service/socket.service.js index 627507d..deb9cb2 100644 --- a/src/service/socket.service.js +++ b/src/service/socket.service.js @@ -82,6 +82,18 @@ class SocketioService { } async sendMessage(payload) { + const cacheWithMatchingPayload = Object.values(this._requestCache).find( + (x) => x.payload.message === payload.message + ); + + if (cacheWithMatchingPayload) { + console.debug(cacheWithMatchingPayload); + + debugger; + + return; + } + const request = uuidv4(); return new Promise((resolve, reject) => { @@ -95,19 +107,19 @@ class SocketioService { if (data.request) { const { reject, resolve, payload } = this._requestCache[data.request]; + delete this._requestCache[data.request]; + if (data.error === "jwt expired") { + resolve(); await refreshToken(); await this.sendMessage({ ...payload, token: VueCookieNext.getCookie("access_token"), }); - - resolve(); + } else { reject(data.error); } - - delete this._requestCache[data.request]; } };