Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/router/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router';
import { useRetrospectiveStore } from '../stores/retrospectiveStore';
import retrospectiveApi from '../services/retrospectiveApi';
import { useWebsocketStore } from '../stores/websocketStore';
import logger from '../services/logger';

export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
Expand Down Expand Up @@ -55,8 +57,16 @@ export const router = createRouter({

router.beforeEach(async (to) => {
const retroStore = useRetrospectiveStore();
const wsStore = useWebsocketStore();
const retroId = to.params.id;

const toName = to.name?.toString() ?? '';

if (!['edit', 'view'].some((a) => toName.includes(a)) && wsStore.websocket) {
logger.debug('Closing current websocket');
wsStore.close();
}

if (
(retroStore.currentRetro === undefined && typeof retroId === 'string') ||
(retroStore.currentRetro?.id && retroStore.currentRetro.id !== retroId && retroId !== undefined)
Expand Down
11 changes: 11 additions & 0 deletions src/services/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const parseText = (type: string, ...args: unknown[]): unknown[] => [`[${type}]`, ...args];

const debug = (...args: unknown[]) => {
if ('DEBUG' in window && window.DEBUG) console.log(...parseText('DEBUG', ...args));
};

const error = (...args: unknown[]) => {
console.error(...parseText('ERROR', ...args));
};

export default { debug, error };
3 changes: 3 additions & 0 deletions src/stores/notifyStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineStore } from 'pinia';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useWebsocketStore } from './websocketStore';
import logger from '../services/logger';

export enum NotificationType {
Success = 'success',
Expand All @@ -24,6 +25,8 @@ export const useNotifyStore = defineStore('notify', () => {

if (destroyConnection) websocket.destroy();

logger.error(`Fatal uncaught error in retro ID ${retroId}. Reason: ${reason}`);

router.push({ name: '500', query: { id: retroId } });

setTimeout(() => {
Expand Down
111 changes: 97 additions & 14 deletions src/stores/websocketStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { API_URL, Endpoints } from '../services';
import { NotificationType, useNotifyStore } from './notifyStore';
import retrospectiveApi from '../services/retrospectiveApi';
import { Question, useRetrospectiveStore } from './retrospectiveStore';
import logger from '../services/logger';

type SocketActions = 'create' | 'update' | 'delete';
type SocketEntity = 'question' | 'retrospective' | 'answer';
type SocketEntity = 'question' | 'retrospective' | 'answer' | 'pong';

type SocketMessage = {
action: SocketActions;
Expand All @@ -22,53 +23,109 @@ export const useWebsocketStore = defineStore('websocket', () => {
const websocket = ref<WebSocket>();
let retrospectiveId = '';

let retryPingTimeout: number;
let heartbeatTimeout: number;
let reconnectTimeout: number;
let retries = 0;

let pingsSent = 0;
let reconnectRetries = 0;

const clearTimers = () => {
clearTimeout(reconnectTimeout);
clearTimeout(heartbeatTimeout);
clearTimeout(retryPingTimeout);
};

const reconnectLogic = () => {
if (retries >= 3) return destroy('The Websocket connection could not be restablished');
if (reconnectRetries >= 3) return destroy('The Websocket connection could not be restablished');

reconnectTimeout = setTimeout(
() => {
retries += 1;
console.log(`Retrying connection for the ${retries} time`);
reconnectRetries += 1;
logger.debug(`Retrying connection for the ${reconnectRetries} time`);
connect(retrospectiveId);
},
1000 * retries || 500,
1000 * reconnectRetries || 500,
);
};

const makePingSequence = () => {
if (!websocket.value) return;

pingsSent += 1;

if (pingsSent > 5) return destroy('The websocket stopped responding');

logger.debug(`<- Sending ping ${pingsSent}`);
websocket.value.send(JSON.stringify({ type: 'ping' }));

retryPingTimeout = setTimeout(() => {
logger.debug(`X-> Pong ${pingsSent} too late! `);
makePingSequence();
}, 500 * pingsSent);
};

const startHeartBeat = () => {
logger.debug('<-> Starting heartbeat sequence in 10 seconds');
heartbeatTimeout = setTimeout(() => {
makePingSequence();
}, 10_000);
};

const ackPong = () => {
if (pingsSent === 0) {
logger.debug(`-> A lazy pong just arrived!`);
return;
}

logger.debug(`-> Pong ${pingsSent} received!`);
pingsSent = 0;
clearTimeout(retryPingTimeout);
startHeartBeat();
};

const onMessage = (message: MessageEvent<string>) => {
const data = JSON.parse(message.data) as SocketMessage;

if (data.type === 'pong') return ackPong();

const functionName = `${data.action}${capitalize(data.type)}` as const;

const toExecute = retroStore[data.type as 'question'][functionName as 'createQuestion'];

if (toExecute === undefined)
if (toExecute === undefined) {
logger.error(
`Unsuported event sent in websocket. Action: "${data.action}", Type: "${data.type}"`,
);

return notifyStore.notify(
`Unsuported event sent in websocket. Action: "${data.action}", Type: "${data.type}"`,
NotificationType.Error,
);
}

toExecute(data.value as Question);
};

const onConnect = async () => {
clearTimeout(reconnectTimeout);
retries = 0;
logger.debug('Websocket connected!');
reconnectRetries = 0;
pingsSent = 0;

const retro = await retrospectiveApi.getRetrospective(retrospectiveId);
if (!retro.error) retroStore.retrospective.updateRetrospective(retro);

startHeartBeat();
notifyStore.notify(`Websocket connected`, NotificationType.Success);
};

const onError = (e: unknown) => {
console.log('Websocket error', e);
logger.error('Websocket error', e);
};

const onClose = () => {
logger.debug('The websocket connection has been closed!');
clearTimers();
notifyStore.notify(`The websocket connection has been closed`, NotificationType.Warning);
reconnectLogic();
};
Expand All @@ -80,6 +137,8 @@ export const useWebsocketStore = defineStore('websocket', () => {
)
return;

clearTimers();

websocket.value = new WebSocket(
`${import.meta.env.PROD ? 'wss' : 'ws'}://${API_URL}${Endpoints.SocketHello}/${retroId}`,
);
Expand All @@ -91,11 +150,35 @@ export const useWebsocketStore = defineStore('websocket', () => {
websocket.value.onclose = onClose;
};

const destroy = (reason?: string) => {
clearTimeout(reconnectTimeout);
const $reset = () => {
clearTimers();
if (!websocket.value) return;

websocket.value.onclose = null;
websocket.value.onerror = null;
websocket.value.onmessage = null;
websocket.value.onopen = null;

retrospectiveId = '';

retryPingTimeout = 0;
heartbeatTimeout = 0;
reconnectTimeout = 0;
pingsSent = 0;
reconnectRetries = 0;

websocket.value?.close(3015, 'Its a panic from my side. Do not take it bad');
websocket.value = undefined;
};

const close = () => {
websocket.value?.close(1000, 'The user left the retrospective');
$reset();
};

const destroy = (reason?: string) => {
websocket.value?.close(3015, 'Its a panic from my side. Do not take it bad');

$reset();

notifyStore.panic(
reason ?? 'The websocket connection was destroyed',
Expand All @@ -104,5 +187,5 @@ export const useWebsocketStore = defineStore('websocket', () => {
);
};

return { websocket, connect, destroy };
return { websocket, connect, destroy, close };
});