Skip to content

Commit

Permalink
fix for websocket dropouts on phase planner and ticket editor view
Browse files Browse the repository at this point in the history
  • Loading branch information
MahtabBukhari committed Feb 18, 2025
1 parent cff91a5 commit 65c7699
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 41 deletions.
10 changes: 8 additions & 2 deletions src/components/auth/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useObserver } from 'mobx-react-lite';
import styled from 'styled-components';
import { observer } from 'mobx-react-lite';
import { AuthProps } from 'people/interfaces';
import { SOCKET_MSG, createSocketInstance } from 'config/socket';
import { SOCKET_MSG, websocketManager } from 'config/socket';
import { useStores } from '../../store';
import { Divider, QR, IconButton } from '../../components/common';
import { useIsMobile } from '../../hooks';
Expand Down Expand Up @@ -107,7 +107,13 @@ function SignIn(props: AuthProps) {
};

useEffect(() => {
const socket: WebSocket = createSocketInstance();
const { socket } = websocketManager;

if (!socket) {
console.error('WebSocket instance not available');
return;
}

socket.onopen = () => {
console.log('Socket connected');
};
Expand Down
115 changes: 90 additions & 25 deletions src/config/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,103 @@ export const SOCKET_MSG = {
budget_success: 'budget_success'
};

let socket: WebSocket | null = null;

export const createSocketInstance = (): WebSocket => {
if (!socket || !socket.OPEN) {
socket = new WebSocket(URL);
// get socket from localStorage
const webssocketToken = localStorage.getItem('websocket_token');
let uniqueID = webssocketToken;

if (uniqueID === null || uniqueID === '') {
uniqueID = uuidv4();
localStorage.setItem('websocket_token', uniqueID!);
class WebSocketManager {
public socket: WebSocket | null = null;
private sessionId: string | null = null;
private lastActiveTime: number = Date.now();
private readonly STALE_THRESHOLD = 5 * 60 * 1000;
private reconnectInterval: number = 3 * 1000;
private reconnectTimeout: NodeJS.Timeout | null = null;

constructor() {
this.initializeFromStorage();
this.setupVisibilityHandler();
this.connect();
this.startStaleCheck();
}

private initializeFromStorage() {
this.sessionId = localStorage.getItem('websocket_token') || uuidv4();
localStorage.setItem('websocket_token', this.sessionId);
}

private setupVisibilityHandler() {
document.addEventListener('visibilitychange', this.handleVisibilityChange);
window.addEventListener('focus', this.checkConnectionFreshness);
}

private handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
this.checkConnectionFreshness();
}
};

socket = new WebSocket(URL + `?uniqueId=${uniqueID}`);
private checkConnectionFreshness = () => {
const now = Date.now();
if (now - this.lastActiveTime > this.STALE_THRESHOLD) {
console.warn('WebSocket connection stale, reconnecting...');
this.reconnect();
}
};

private startStaleCheck() {
setInterval(() => {
this.checkConnectionFreshness();
}, this.STALE_THRESHOLD);
}

socket.onclose = () => {
console.log('WebSocket connection closed from index');
setTimeout(createSocketInstance, 500);
private updateLastActive() {
this.lastActiveTime = Date.now();
localStorage.setItem('websocket_last_active', this.lastActiveTime.toString());
}

public connect() {
if (this.socket) {
this.socket.close();
}

this.socket = new WebSocket(`${URL}?uniqueId=${this.sessionId}`);

this.socket.onopen = () => {
console.log('WebSocket connected');
this.updateLastActive();
};

this.socket.onmessage = (event: MessageEvent) => {
this.updateLastActive();
try {
const data = JSON.parse(event.data);
if (data.msg === SOCKET_MSG.user_connect) {
this.sessionId = data.body || this.sessionId;
localStorage.setItem('websocket_token', this.sessionId as string);
}
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
};
socket.onerror = (error: any) => {

this.socket.onerror = (error: Event) => {
console.error('WebSocket error:', error);
setTimeout(createSocketInstance, 1000);
this.scheduleReconnect();
};

this.socket.onclose = () => {
console.warn('WebSocket disconnected, attempting reconnect...');
this.scheduleReconnect();
};
}

return socket;
};
private scheduleReconnect() {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
this.reconnectTimeout = setTimeout(() => this.reconnect(), this.reconnectInterval);
}

export const getSocketInstance = (): WebSocket => {
if (!socket) {
throw new Error('Socket instance not created. Call createSocketInstance first.');
public reconnect() {
console.log('Reconnecting WebSocket...');
this.connect();
}
return socket;
};
}

export const websocketManager = new WebSocketManager();
9 changes: 7 additions & 2 deletions src/people/hiveChat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { observer } from 'mobx-react-lite';
import { useHistory, useParams } from 'react-router-dom';
import { ChatMessage } from 'store/interface';
import { useStores } from 'store';
import { createSocketInstance } from 'config/socket';
import { websocketManager } from 'config/socket';
import { SOCKET_MSG } from 'config/socket';
import styled from 'styled-components';
import { EuiLoadingSpinner } from '@elastic/eui';
Expand Down Expand Up @@ -378,7 +378,12 @@ export const HiveChatView: React.FC = observer(() => {

useEffect(() => {
// eslint-disable-next-line prefer-const
let socket = createSocketInstance();
const { socket } = websocketManager;

if (!socket) {
console.error('WebSocket instance not available');
return;
}

socket.onmessage = async (event: MessageEvent) => {
console.log('Raw websocket message received:', event.data);
Expand Down
10 changes: 7 additions & 3 deletions src/people/widgetViews/PhasePlannerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import {
Data,
Label
} from 'pages/tickets/style';
import { SOCKET_MSG } from 'config/socket';
import { createSocketInstance } from 'config/socket';
import { SOCKET_MSG, websocketManager } from 'config/socket';
import SidebarComponent from 'components/common/SidebarComponent.tsx';
import styled from 'styled-components';
import { phaseTicketStore } from '../../store/phase';
Expand Down Expand Up @@ -102,7 +101,12 @@ const PhasePlannerView: React.FC = observer(() => {
const languageString = '';

useEffect(() => {
const socket = createSocketInstance();
const { socket } = websocketManager;

if (!socket) {
console.error('WebSocket instance not available');
return;
}

socket.onopen = () => {
console.log('Socket connected in Phase Planner');
Expand Down
9 changes: 7 additions & 2 deletions src/people/widgetViews/WorkspaceFeature.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import MaterialIcon from '@material/react-material-icon';
import { EuiOverlayMask, EuiModalHeader, EuiModalFooter, EuiText } from '@elastic/eui';
import { Box } from '@mui/system';
import { userHasRole } from 'helpers/helpers-extended';
import { createSocketInstance, SOCKET_MSG } from '../../config/socket.ts';
import { SOCKET_MSG, websocketManager } from '../../config/socket.ts';
import { useDeleteConfirmationModal } from '../../components/common';
import SidebarComponent from '../../components/common/SidebarComponent.tsx';
import {
Expand Down Expand Up @@ -497,7 +497,12 @@ const WorkspaceFeature = () => {
}, [getWorkspaceData]);

useEffect(() => {
let socket = createSocketInstance();
const { socket } = websocketManager;

if (!socket) {
console.error('WebSocket instance not available');
return;
}

socket.onmessage = async (event: MessageEvent) => {
console.log('Raw websocket message received:', event.data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EuiText, EuiFieldText, EuiGlobalToastList, EuiLoadingSpinner } from '@e
import { observer } from 'mobx-react-lite';
import moment from 'moment';
import { isInvoiceExpired, userCanManageBounty } from 'helpers';
import { SOCKET_MSG, createSocketInstance } from 'config/socket';
import { SOCKET_MSG, websocketManager } from 'config/socket';
import { Box } from '@mui/system';
import { bountyReviewStore } from 'store/bountyReviewStore';
import { uiStore } from 'store/ui';
Expand Down Expand Up @@ -601,7 +601,13 @@ function MobileView(props: CodingBountiesProps) {
}
};

const socket: WebSocket = createSocketInstance();
const { socket } = websocketManager;

if (!socket) {
console.error('WebSocket instance not available');
return;
}

socket.onopen = () => {
console.log('Socket connected');
};
Expand Down
10 changes: 7 additions & 3 deletions src/people/widgetViews/workspace/WorkspaceTicketCreateView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import MaterialIcon from '@material/react-material-icon';
import { useStores } from 'store';
import { v4 as uuidv4 } from 'uuid';
import { FeatureBody, FeatureDataWrap, FieldWrap } from 'pages/tickets/style';
import { SOCKET_MSG } from 'config/socket';
import { createSocketInstance } from 'config/socket';
import { SOCKET_MSG, websocketManager } from 'config/socket';
import { EuiGlobalToastList } from '@elastic/eui';
import NewTicketEditor from 'components/common/TicketEditor/NewTicketEditor';
import SidebarComponent from 'components/common/SidebarComponent';
Expand Down Expand Up @@ -165,7 +164,12 @@ const WorkspaceTicketCreateView: React.FC = observer(() => {
}, [selectedFeature, main]);

useEffect(() => {
const socket = createSocketInstance();
const { socket } = websocketManager;

if (!socket) {
console.error('WebSocket instance not available');
return;
}

socket.onopen = () => {
console.log('Socket connected in Workspace Ticket Create View');
Expand Down
9 changes: 7 additions & 2 deletions src/people/widgetViews/workspace/WorkspaceTicketView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useParams, useHistory } from 'react-router-dom';
import { useStores } from 'store';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import MaterialIcon from '@material/react-material-icon';
import { createSocketInstance, SOCKET_MSG } from 'config/socket';
import { SOCKET_MSG, websocketManager } from 'config/socket';
import WorkspaceTicketEditor from 'components/common/TicketEditor/WorkspaceTicketEditor';
import SidebarComponent from 'components/common/SidebarComponent.tsx';
import styled from 'styled-components';
Expand Down Expand Up @@ -60,7 +60,12 @@ const WorkspaceTicketView: React.FC = observer(() => {
const [collapsed, setCollapsed] = useState(false);

useEffect(() => {
const socket = createSocketInstance();
const { socket } = websocketManager;

if (!socket) {
console.error('WebSocket instance not available');
return;
}

socket.onopen = () => {
console.log('Socket connected in Workspace Ticket View');
Expand Down

0 comments on commit 65c7699

Please sign in to comment.