Add socket reconnect logic

This commit is contained in:
Dane Everitt 2019-05-09 22:42:53 -07:00
parent d79fe6982f
commit d280a91115
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
3 changed files with 113 additions and 22 deletions

View file

@ -34,11 +34,6 @@
Databases Databases
</router-link> </router-link>
</li> </li>
<li>
<router-link :to="{ name: 'server-network' }">
Networking
</router-link>
</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -49,7 +44,7 @@
</div> </div>
<div class="fixed pin-r pin-b m-6 max-w-sm" v-show="connectionError"> <div class="fixed pin-r pin-b m-6 max-w-sm" v-show="connectionError">
<div class="alert error"> <div class="alert error">
There was an error while attempting to connect to the Daemon websocket. Error reported was: "{{connectionError.message}}" There was an error while attempting to connect to the Daemon websocket. Error: {{connectionError}}
</div> </div>
</div> </div>
</div> </div>

File diff suppressed because one or more lines are too long

View file

@ -24,6 +24,21 @@ export default class SocketioConnector {
*/ */
store: Store<any> | undefined; store: Store<any> | undefined;
/**
* Tracks a reconnect attempt for the websocket. Will gradually back off on attempts
* after a certain period of time has elapsed.
*/
private reconnectTimeout: any;
/**
* Tracks the number of reconnect attempts which is used to determine the backoff
* throttle for connections.
*/
private reconnectAttempts: number = 0;
private socketProtocol?: string;
private socketUrl?: string;
constructor(store: Store<any> | undefined) { constructor(store: Store<any> | undefined) {
this.socket = null; this.socket = null;
this.store = store; this.store = store;
@ -32,15 +47,23 @@ export default class SocketioConnector {
/** /**
* Initialize a new Socket connection. * Initialize a new Socket connection.
*/ */
connect(url: string, protocols?: string | string[]): void { public connect(url: string, protocol?: string): void {
this.socket = new WebSocket(url, protocols); this.socketUrl = url;
this.socketProtocol = protocol;
this.connectToSocket()
.then(socket => {
this.socket = socket;
this.emitAndPassToStore(SOCKET_CONNECT);
this.registerEventListeners(); this.registerEventListeners();
})
.catch(() => this.reconnectToSocket());
} }
/** /**
* Return the socket instance we are working with. * Return the socket instance we are working with.
*/ */
instance(): WebSocket | null { public instance(): WebSocket | null {
return this.socket; return this.socket;
} }
@ -48,7 +71,7 @@ export default class SocketioConnector {
* Sends an event along to the websocket. If there is no active connection, a void * Sends an event along to the websocket. If there is no active connection, a void
* result is returned. * result is returned.
*/ */
emit(event: string, payload?: string | Array<string>): void | false { public emit(event: string, payload?: string | Array<string>): void | false {
if (!this.socket) { if (!this.socket) {
return false return false
} }
@ -62,23 +85,23 @@ export default class SocketioConnector {
* Register the event listeners for this socket including user-defined ones in the store as * Register the event listeners for this socket including user-defined ones in the store as
* well as global system events from Socekt.io. * well as global system events from Socekt.io.
*/ */
registerEventListeners() { protected registerEventListeners() {
if (!this.socket) { if (!this.socket) {
return; return;
} }
this.socket.onopen = () => this.emitAndPassToStore(SOCKET_CONNECT); this.socket.onclose = () => {
this.socket.onclose = () => this.emitAndPassToStore(SOCKET_DISCONNECT); this.reconnectToSocket();
this.emitAndPassToStore(SOCKET_DISCONNECT);
};
this.socket.onerror = () => { this.socket.onerror = () => {
// @todo reconnect? if (this.socket && this.socket.readyState !== WebSocket.OPEN) {
if (this.socket && this.socket.readyState !== 1) {
this.emitAndPassToStore(SOCKET_ERROR, ['Failed to connect to websocket.']); this.emitAndPassToStore(SOCKET_ERROR, ['Failed to connect to websocket.']);
} }
}; };
this.socket.onmessage = (wse): void => { this.socket.onmessage = (wse): void => {
console.log('Socket message:', wse.data);
try { try {
let {event, args}: WingsWebsocketResponse = JSON.parse(wse.data); let {event, args}: WingsWebsocketResponse = JSON.parse(wse.data);
@ -91,10 +114,83 @@ export default class SocketioConnector {
}; };
} }
/**
* Performs an actual socket connection, wrapped as a Promise for an easier interface.
*/
protected connectToSocket(): Promise<WebSocket> {
return new Promise((resolve, reject) => {
let hasReturned = false;
const socket = new WebSocket(this.socketUrl!, this.socketProtocol);
socket.onopen = () => {
if (hasReturned) {
socket && socket.close();
}
hasReturned = true;
this.resetConnectionAttempts();
resolve(socket);
};
const rejectFunc = () => {
if (!hasReturned) {
hasReturned = true;
this.emitAndPassToStore(SOCKET_ERROR, ['Failed to connect to websocket.']);
reject();
}
};
socket.onerror = rejectFunc;
socket.onclose = rejectFunc;
});
}
/**
* Attempts to reconnect to the socket instance if it becomes disconnected.
*/
private reconnectToSocket() {
const { socket } = this;
if (!socket) {
return;
}
// Clear the existing timeout if one exists for some reason.
this.reconnectTimeout && clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = setTimeout(() => {
console.warn(`Attempting to reconnect to websocket [${this.reconnectAttempts}]...`);
this.reconnectAttempts++;
this.connect(this.socketUrl!, this.socketProtocol);
}, this.getIntervalTimeout());
}
private resetConnectionAttempts() {
this.reconnectTimeout && clearTimeout(this.reconnectTimeout);
this.reconnectAttempts = 0;
}
/**
* Determine the amount of time we should wait before attempting to reconnect to the socket.
*/
private getIntervalTimeout(): number {
if (this.reconnectAttempts < 10) {
return 50;
} else if (this.reconnectAttempts < 25) {
return 500;
} else if (this.reconnectAttempts < 50) {
return 1000;
}
return 2500;
}
/** /**
* Emits the event over the event emitter and also passes it along to the vuex store. * Emits the event over the event emitter and also passes it along to the vuex store.
*/ */
emitAndPassToStore(event: string, payload?: Array<string>) { private emitAndPassToStore(event: string, payload?: Array<string>) {
payload ? SocketEmitter.emit(event, ...payload) : SocketEmitter.emit(event); payload ? SocketEmitter.emit(event, ...payload) : SocketEmitter.emit(event);
this.passToStore(event, payload); this.passToStore(event, payload);
} }
@ -102,7 +198,7 @@ export default class SocketioConnector {
/** /**
* Pass event calls off to the Vuex store if there is a corresponding function. * Pass event calls off to the Vuex store if there is a corresponding function.
*/ */
passToStore(event: string, payload?: Array<string>) { private passToStore(event: string, payload?: Array<string>) {
if (!this.store) { if (!this.store) {
return; return;
} }
@ -126,7 +222,7 @@ export default class SocketioConnector {
}); });
} }
unwrap(args: Array<string>) { private unwrap(args: Array<string>) {
return (args && args.length <= 1) ? args[0] : args; return (args && args.length <= 1) ? args[0] : args;
} }
} }