UNPKG

7.01 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2018 TypeFox and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import { inject, injectable, optional, postConstruct } from 'inversify';
18import { ILogger } from '../common/logger';
19import { Event, Emitter } from '../common/event';
20import { DefaultFrontendApplicationContribution } from './frontend-application';
21import { StatusBar, StatusBarAlignment } from './status-bar/status-bar';
22import { WebSocketConnectionProvider } from './messaging/ws-connection-provider';
23import { Disposable, DisposableCollection, nls } from '../common';
24
25/**
26 * Service for listening on backend connection changes.
27 */
28export const ConnectionStatusService = Symbol('ConnectionStatusService');
29export interface ConnectionStatusService {
30
31 /**
32 * The actual connection status.
33 */
34 readonly currentStatus: ConnectionStatus;
35
36 /**
37 * Clients can listen on connection status change events.
38 */
39 readonly onStatusChange: Event<ConnectionStatus>;
40
41}
42
43/**
44 * The connection status.
45 */
46export enum ConnectionStatus {
47
48 /**
49 * Connected to the backend.
50 */
51 ONLINE,
52
53 /**
54 * The connection is lost between frontend and backend.
55 */
56 OFFLINE
57}
58
59@injectable()
60export class ConnectionStatusOptions {
61
62 static DEFAULT: ConnectionStatusOptions = {
63 offlineTimeout: 5000,
64 };
65
66 /**
67 * Timeout in milliseconds before the application is considered offline. Must be a positive integer.
68 */
69 readonly offlineTimeout: number;
70
71}
72
73export const PingService = Symbol('PingService');
74export interface PingService {
75 ping(): Promise<void>;
76}
77
78@injectable()
79export abstract class AbstractConnectionStatusService implements ConnectionStatusService, Disposable {
80
81 protected readonly statusChangeEmitter = new Emitter<ConnectionStatus>();
82
83 protected connectionStatus: ConnectionStatus = ConnectionStatus.ONLINE;
84
85 @inject(ILogger)
86 protected logger: ILogger;
87
88 constructor(@inject(ConnectionStatusOptions) @optional() protected readonly options: ConnectionStatusOptions = ConnectionStatusOptions.DEFAULT) { }
89
90 get onStatusChange(): Event<ConnectionStatus> {
91 return this.statusChangeEmitter.event;
92 }
93
94 get currentStatus(): ConnectionStatus {
95 return this.connectionStatus;
96 }
97
98 dispose(): void {
99 this.statusChangeEmitter.dispose();
100 }
101
102 protected updateStatus(success: boolean): void {
103 const previousStatus = this.connectionStatus;
104 const newStatus = success ? ConnectionStatus.ONLINE : ConnectionStatus.OFFLINE;
105 if (previousStatus !== newStatus) {
106 this.connectionStatus = newStatus;
107 this.fireStatusChange(newStatus);
108 }
109 }
110
111 protected fireStatusChange(status: ConnectionStatus): void {
112 this.statusChangeEmitter.fire(status);
113 }
114
115}
116
117@injectable()
118export class FrontendConnectionStatusService extends AbstractConnectionStatusService {
119
120 private scheduledPing: number | undefined;
121
122 @inject(WebSocketConnectionProvider) protected readonly wsConnectionProvider: WebSocketConnectionProvider;
123 @inject(PingService) protected readonly pingService: PingService;
124
125 @postConstruct()
126 protected init(): void {
127 this.wsConnectionProvider.onSocketDidOpen(() => {
128 this.updateStatus(true);
129 this.schedulePing();
130 });
131 this.wsConnectionProvider.onSocketDidClose(() => {
132 this.clearTimeout(this.scheduledPing);
133 this.updateStatus(false);
134 });
135 this.wsConnectionProvider.onIncomingMessageActivity(() => {
136 // natural activity
137 this.updateStatus(true);
138 this.schedulePing();
139 });
140 }
141
142 protected schedulePing(): void {
143 this.clearTimeout(this.scheduledPing);
144 this.scheduledPing = this.setTimeout(async () => {
145 await this.performPingRequest();
146 this.schedulePing();
147 }, this.options.offlineTimeout);
148 }
149
150 protected async performPingRequest(): Promise<void> {
151 try {
152 await this.pingService.ping();
153 this.updateStatus(true);
154 } catch (e) {
155 this.updateStatus(false);
156 await this.logger.error(e);
157 }
158 }
159
160 // eslint-disable-next-line @typescript-eslint/no-explicit-any
161 protected setTimeout(handler: (...args: any[]) => void, timeout: number): number {
162 return window.setTimeout(handler, timeout);
163 }
164
165 protected clearTimeout(handle?: number): void {
166 if (handle !== undefined) {
167 window.clearTimeout(handle);
168 }
169 }
170}
171
172@injectable()
173export class ApplicationConnectionStatusContribution extends DefaultFrontendApplicationContribution {
174
175 protected readonly toDisposeOnOnline = new DisposableCollection();
176
177 constructor(
178 @inject(ConnectionStatusService) protected readonly connectionStatusService: ConnectionStatusService,
179 @inject(StatusBar) protected readonly statusBar: StatusBar,
180 @inject(ILogger) protected readonly logger: ILogger
181 ) {
182 super();
183 this.connectionStatusService.onStatusChange(state => this.onStateChange(state));
184 }
185
186 protected onStateChange(state: ConnectionStatus): void {
187 switch (state) {
188 case ConnectionStatus.OFFLINE: {
189 this.handleOffline();
190 break;
191 }
192 case ConnectionStatus.ONLINE: {
193 this.handleOnline();
194 break;
195 }
196 }
197 }
198
199 private statusbarId = 'connection-status';
200
201 protected handleOnline(): void {
202 this.toDisposeOnOnline.dispose();
203 }
204
205 protected handleOffline(): void {
206 this.statusBar.setElement(this.statusbarId, {
207 alignment: StatusBarAlignment.LEFT,
208 text: nls.localize('theia/core/offline', 'Offline'),
209 tooltip: nls.localize('theia/localize/offlineTooltip', 'Cannot connect to backend.'),
210 priority: 5000
211 });
212 this.toDisposeOnOnline.push(Disposable.create(() => this.statusBar.removeElement(this.statusbarId)));
213 document.body.classList.add('theia-mod-offline');
214 this.toDisposeOnOnline.push(Disposable.create(() => document.body.classList.remove('theia-mod-offline')));
215 }
216}