Skip to content

Commit

Permalink
[#1055] Expired Websocket Connection
Browse files Browse the repository at this point in the history
closes #1055
  • Loading branch information
bitboxer committed Mar 9, 2021
1 parent d61ce1f commit 29145d9
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 40 deletions.
22 changes: 22 additions & 0 deletions frontend/chat-plugin/src/components/chat/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,25 @@
.messages::-webkit-scrollbar {
display: none;
}

.connectedContainer {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
position: relative;
}

.disconnectedOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(1, 1, 1, 0.7);
display: flex;
justify-content: center;
align-items: center;
color: white;
}
69 changes: 38 additions & 31 deletions frontend/chat-plugin/src/components/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import {useState, useEffect} from 'react';
import {IMessage} from '@stomp/stompjs';

import WebSocket from '../../websocket';
import WebSocket, {ConnectionState} from '../../websocket';
import MessageProp from '../../components/message';
import InputBarProp from '../../components/inputBar';
import AiryInputBar from '../../airyRenderProps/AiryInputBar';
Expand All @@ -16,7 +16,6 @@ import AiryBubble from '../../airyRenderProps/AiryBubble';
import {MessagePayload, SenderType, MessageState, isFromContact, Message} from 'httpclient';
import {SourceMessage, CommandUnion} from 'render';
import {MessageInfoWrapper} from 'render/components/MessageInfoWrapper';
import {getResumeTokenFromStorage} from '../../storage';
/* eslint-disable @typescript-eslint/no-var-requires */
const camelcaseKeys = require('camelcase-keys');

Expand All @@ -42,9 +41,12 @@ const Chat = (props: Props) => {
const [isChatHidden, setIsChatHidden] = useState(true);
const [messages, setMessages] = useState<Message[]>([defaultWelcomeMessage]);
const [messageString, setMessageString] = useState('');
const [connectionState, setConnectionState] = useState(null);

useEffect(() => {
ws = new WebSocket(props.channelId, onReceive, setInitialMessages, getResumeTokenFromStorage(props.channelId));
ws = new WebSocket(props.channelId, onReceive, setInitialMessages, (state: ConnectionState) => {
setConnectionState(state);
});
ws.start().catch(error => {
console.error(error);
setInstallError(error.message);
Expand Down Expand Up @@ -143,35 +145,40 @@ const Chat = (props: Props) => {
{!isChatHidden && (
<div className={`${style.container} ${styleFor(animation)}`}>
<HeaderBarProp render={headerBar} />
<div className={style.chat}>
<div id="messages" className={style.messages}>
{messages.map((message, index: number) => {
const nextMessage = messages[index + 1];
const lastInGroup = nextMessage ? isFromContact(message) !== isFromContact(nextMessage) : true;

return (
<MessageProp
key={message.id}
render={
props.airyMessageProp
? () => props.airyMessageProp(ctrl)
: () => (
<MessageInfoWrapper fromContact={isFromContact(message)} isChatPlugin={true}>
<SourceMessage
message={message}
source="chat_plugin"
lastInGroup={lastInGroup}
invertSides={true}
commandCallback={commandCallback}
/>
</MessageInfoWrapper>
)
}
/>
);
})}
<div className={style.connectedContainer}>
<div className={style.chat}>
<div id="messages" className={style.messages}>
{messages.map((message, index: number) => {
const nextMessage = messages[index + 1];
const lastInGroup = nextMessage ? isFromContact(message) !== isFromContact(nextMessage) : true;

return (
<MessageProp
key={message.id}
render={
props.airyMessageProp
? () => props.airyMessageProp(ctrl)
: () => (
<MessageInfoWrapper fromContact={isFromContact(message)} isChatPlugin={true}>
<SourceMessage
message={message}
source="chat_plugin"
lastInGroup={lastInGroup}
invertSides={true}
commandCallback={commandCallback}
/>
</MessageInfoWrapper>
)
}
/>
);
})}
</div>
<InputBarProp render={inputBar} />
{connectionState === ConnectionState.Disconnected && (
<div className={style.disconnectedOverlay}>Reconnecting...</div>
)}
</div>
<InputBarProp render={inputBar} />
</div>
</div>
)}
Expand Down
51 changes: 42 additions & 9 deletions frontend/chat-plugin/src/websocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,51 @@ import 'regenerator-runtime/runtime';
import {start, getResumeToken, sendMessage} from '../api';
import {SuggestionResponse, TextContent} from 'render/providers/chatplugin/chatPluginModel';
import {Message} from 'httpclient';
import {resetStorage} from '../storage';
import {getResumeTokenFromStorage, resetStorage} from '../storage';

/* eslint-disable @typescript-eslint/no-var-requires */
const camelcaseKeys = require('camelcase-keys');

declare const window: {
declare global {
interface Window {
airy: {
host: string;
channelId: string;
noTLS: boolean;
};
};
}
}

const API_HOST = window.airy ? window.airy.host : 'chatplugin.airy';
// https: -> wss: and http: -> ws:
const protocol = location.protocol.replace('http', 'ws');

export enum ConnectionState{
Connected = "CONNECTED",
Disconnected = "DISCONNECTED"
}

class WebSocket {
client: Client;
channelId: string;
token: string;
resumeToken: string;
setInitialMessages: (messages: Array<Message>) => void;
onReceive: messageCallbackType;
reconnectTimeout: number;
isConnected: boolean;
updateConnectionState: (state: ConnectionState) => void;

constructor(
channelId: string,
onReceive: messageCallbackType,
setInitialMessages: (messages: Array<Message>) => void,
resumeToken?: string
updateConnectionState: (state: ConnectionState) => void
) {
this.channelId = channelId;
this.onReceive = onReceive;
this.resumeToken = resumeToken;
this.setInitialMessages = setInitialMessages;
this.isConnected = false;
this.updateConnectionState = updateConnectionState;
}

connect = (token: string) => {
Expand All @@ -50,12 +61,13 @@ class WebSocket {
debug: function (str) {
console.info(str);
},
reconnectDelay: 5000,
reconnectDelay: 0,
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});

this.client.onConnect = this.onConnect;
this.client.onWebSocketClose = this.onWebSocketClose;

this.client.onStompError = function (frame: IFrame) {
console.error('Broker reported error: ' + frame.headers['message']);
Expand All @@ -68,7 +80,8 @@ class WebSocket {
onSend = (message: TextContent | SuggestionResponse) => sendMessage(message, this.token);

start = async () => {
const response = await start(this.channelId, this.resumeToken);
const resumeToken = getResumeTokenFromStorage(this.channelId)
const response = await start(this.channelId, resumeToken);
if (response.token && response.messages) {
this.connect(response.token);
this.setInitialMessages(
Expand All @@ -77,7 +90,7 @@ class WebSocket {
sentAt: new Date(message.sent_at),
}))
);
if (!this.resumeToken) {
if (!resumeToken) {
await getResumeToken(this.channelId, this.token);
}
} else {
Expand All @@ -87,7 +100,27 @@ class WebSocket {

onConnect = () => {
this.client.subscribe('/user/queue/message', this.onReceive);
this.isConnected = true;
clearTimeout(this.reconnectTimeout)
this.updateConnectionState(ConnectionState.Connected)
};

tryReconnect = () => {
this.reconnectTimeout = window.setTimeout(this.reconnect, 5000)
}

reconnect = () => {
if (!this.isConnected) {
this.reconnectTimeout = window.setTimeout(this.reconnect, 5000)
this.start();
}
}

onWebSocketClose = () => {
this.isConnected = false;
this.updateConnectionState(ConnectionState.Disconnected)
this.tryReconnect()
}
}

export default WebSocket;

0 comments on commit 29145d9

Please sign in to comment.