-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GH-1155: Added support to handle the connection issues gracefully.
Signed-off-by: Akos Kitta <kittaakos@gmail.com>
- Loading branch information
Showing
12 changed files
with
829 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
232 changes: 232 additions & 0 deletions
232
packages/core/src/browser/frontend-connection-status-service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
/* | ||
* Copyright (C) 2018 TypeFox and others. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
*/ | ||
|
||
import { inject, injectable, optional } from 'inversify'; | ||
import { ILogger } from '../common/logger'; | ||
import { Endpoint } from './endpoint'; | ||
import { IdleService } from '../common/idle-service'; | ||
import { AbstractDialog, DialogProps } from './dialogs'; | ||
import { StatusBar, StatusBarAlignment } from './status-bar/status-bar'; | ||
import { FrontendApplicationContribution, DefaultFrontendApplicationContribution } from './frontend-application'; | ||
import { AbstractConnectionStatusService, ConnectionState, ConnectionStatusOptions, ConnectionStatus, ConnectionStatusService } from '../common/connection-status-service'; | ||
|
||
@injectable() | ||
export class FrontendConnectionStatusOptions extends ConnectionStatusOptions { | ||
|
||
static DEFAULT: FrontendConnectionStatusOptions = { | ||
...ConnectionStatusOptions.DEFAULT, | ||
requestTimeout: 1000, | ||
}; | ||
|
||
/** | ||
* Timeout for the HTTP GET request in milliseconds. Must be a positive integer. | ||
*/ | ||
readonly requestTimeout: number; | ||
|
||
} | ||
|
||
@injectable() | ||
export abstract class FrontendConnectionStatusService extends AbstractConnectionStatusService<number> implements FrontendApplicationContribution { | ||
|
||
private readonly endpointUrl: string; | ||
|
||
constructor( | ||
@inject(FrontendConnectionStatusOptions) @optional() protected readonly options: FrontendConnectionStatusOptions = FrontendConnectionStatusOptions.DEFAULT, | ||
@inject(IdleService) protected idleService: IdleService, | ||
@inject(ILogger) protected readonly logger: ILogger | ||
) { | ||
super(options, idleService, logger); | ||
this.endpointUrl = new Endpoint().getRestUrl().toString(); | ||
} | ||
|
||
onStart() { | ||
this.start(); | ||
} | ||
|
||
onStop() { | ||
this.stop(); | ||
} | ||
|
||
protected checkAlive(): Promise<boolean> { | ||
return new Promise<boolean>(resolve => { | ||
const handle = (success: boolean) => { | ||
return resolve(success); | ||
}; | ||
const xhr = new XMLHttpRequest(); | ||
xhr.timeout = this.options.requestTimeout; | ||
xhr.onreadystatechange = () => { | ||
const { readyState, status } = xhr; | ||
if (readyState === XMLHttpRequest.DONE) { | ||
handle(status === 200); | ||
} | ||
}; | ||
xhr.onerror = () => handle(false); | ||
xhr.ontimeout = () => handle(false); | ||
xhr.open('GET', `${this.endpointUrl}/alive`); | ||
try { | ||
xhr.send(); | ||
} catch { | ||
handle(false); | ||
} | ||
}); | ||
} | ||
|
||
protected setTimeout(handler: (...args: any[]) => void, timeout: number): number { | ||
return window.setTimeout(handler, timeout); | ||
} | ||
|
||
protected clearTimeout(handle: number): void { | ||
window.clearTimeout(handle); | ||
} | ||
|
||
} | ||
|
||
@injectable() | ||
export class ConnectionStatusStatusBarContribution extends DefaultFrontendApplicationContribution { | ||
|
||
constructor( | ||
@inject(ConnectionStatusService) protected readonly connectionStatusService: ConnectionStatusService, | ||
@inject(StatusBar) protected statusBar: StatusBar | ||
) { | ||
super(); | ||
this.connectionStatusService.onStatusChange(status => this.onStatusChange(status)); | ||
} | ||
|
||
protected onStatusChange(status: ConnectionStatus) { | ||
this.statusBar.removeElement('connection-status'); | ||
const text = `$(${this.getStatusIcon(status)})`; | ||
const tooltip = this.getStatusTooltip(status); | ||
this.statusBar.setElement('connection-status', { | ||
alignment: StatusBarAlignment.RIGHT, | ||
text, | ||
priority: 0, | ||
tooltip | ||
}); | ||
} | ||
|
||
protected getStatusIcon(status: ConnectionStatus) { | ||
if (status.state === ConnectionState.IDLE) { | ||
return 'bed'; | ||
} | ||
const { health } = status; | ||
if (health === undefined || health === 0) { | ||
return 'exclamation-circle'; | ||
} | ||
if (health < 25) { | ||
return 'frown-o'; | ||
} | ||
if (health < 50) { | ||
return 'meh-o'; | ||
} | ||
return 'smile-o'; | ||
} | ||
|
||
protected getStatusTooltip(status: ConnectionStatus) { | ||
if (status.state === ConnectionState.IDLE) { | ||
return 'Idle'; | ||
} | ||
return status.health ? `Online [Connection health: ${status.health}%]` : 'Offline'; | ||
} | ||
|
||
} | ||
|
||
@injectable() | ||
export class ApplicationConnectionStatusContribution extends DefaultFrontendApplicationContribution { | ||
|
||
private dialog: ConnectionStatusDialog | undefined; | ||
private state = ConnectionState.ONLINE; | ||
|
||
constructor( | ||
@inject(ConnectionStatusService) protected readonly connectionStatusService: ConnectionStatusService, | ||
@inject(ILogger) protected readonly logger: ILogger | ||
) { | ||
super(); | ||
this.connectionStatusService.onStatusChange(status => this.onStatusChange(status)); | ||
} | ||
|
||
protected onStatusChange(status: ConnectionStatus): void { | ||
if (this.state !== status.state) { | ||
this.state = status.state; | ||
switch (status.state) { | ||
case ConnectionState.OFFLINE: { | ||
this.handleOffline(); | ||
break; | ||
} | ||
case ConnectionState.ONLINE: { | ||
this.handleOnline(); | ||
break; | ||
} | ||
case ConnectionState.IDLE: { | ||
this.handleIdle(); | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
protected getOrCreateDialog(content: string): ConnectionStatusDialog { | ||
if (this.dialog === undefined) { | ||
this.dialog = new ConnectionStatusDialog({ | ||
title: 'Not connected', | ||
content | ||
}); | ||
} | ||
return this.dialog; | ||
} | ||
|
||
protected handleOnline() { | ||
const message = 'Successfully reconnected to the backend.'; | ||
this.logger.info(message); | ||
if (this.dialog !== undefined) { | ||
this.dialog.dispose(); | ||
this.dialog = undefined; | ||
} | ||
} | ||
|
||
protected handleOffline() { | ||
const message = 'The application connection to the backend is lost. Attempting to reconnect...'; | ||
if (this.dialog === undefined) { | ||
this.logger.error(message); | ||
this.getOrCreateDialog(message).open(); | ||
} else { | ||
this.dialog.updateContent(message); | ||
} | ||
} | ||
|
||
protected handleIdle() { | ||
if (this.dialog !== undefined) { | ||
const message = 'The application connection to the backend is lost.'; | ||
this.dialog.updateContent(message); | ||
} | ||
} | ||
|
||
} | ||
|
||
export class ConnectionStatusDialog extends AbstractDialog<void> { | ||
|
||
public readonly value: void; | ||
protected readonly contentText: Text; | ||
|
||
constructor(dialogProps: DialogProps & { content: string }) { | ||
super(dialogProps); | ||
// Just to remove the X, so that the dialog cannot be closed by the user. | ||
this.closeCrossNode.remove(); | ||
this.contentText = document.createTextNode(dialogProps.content); | ||
this.contentNode.appendChild(this.contentText); | ||
} | ||
|
||
protected onAfterAttach() { | ||
// NOOP. | ||
// We need disable the key listener for escape and return so that the dialog cannot be closed by the user. | ||
} | ||
|
||
public updateContent(content: string) { | ||
this.contentText.data = content; | ||
this.update(); | ||
} | ||
|
||
} |
Oops, something went wrong.