@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
177 lines • 8.83 kB
JavaScript
// *****************************************************************************
// Copyright (C) 2023 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebsocketFrontendConnectionService = void 0;
const tslib_1 = require("tslib");
const inversify_1 = require("inversify");
const default_messaging_service_1 = require("./default-messaging-service");
const socket_write_buffer_1 = require("../../common/messaging/socket-write-buffer");
const channel_1 = require("../../common/message-rpc/channel");
const uint8_array_message_buffer_1 = require("../../common/message-rpc/uint8-array-message-buffer");
const backend_application_config_provider_1 = require("../backend-application-config-provider");
const websocket_endpoint_1 = require("./websocket-endpoint");
const connection_management_1 = require("../../common/messaging/connection-management");
const common_1 = require("../../common");
let WebsocketFrontendConnectionService = class WebsocketFrontendConnectionService {
constructor() {
this.wsHandlers = new default_messaging_service_1.ConnectionHandlers();
this.connectionsByFrontend = new Map();
this.closeTimeouts = new Map();
this.channelsMarkedForClose = new Set();
}
registerConnectionHandler(spec, callback) {
this.websocketServer.registerConnectionHandler(spec, (params, socket) => this.handleConnection(socket, channel => callback(params, channel)));
}
async handleConnection(socket, channelCreatedHandler) {
// eslint-disable-next-line prefer-const
let reconnectListener;
const initialConnectListener = (frontEndId) => {
socket.off(connection_management_1.ConnectionManagementMessages.INITIAL_CONNECT, initialConnectListener);
socket.off(connection_management_1.ConnectionManagementMessages.RECONNECT, reconnectListener);
if (this.connectionsByFrontend.has(frontEndId)) {
this.closeConnection(frontEndId, 'reconnecting same front end');
}
const channel = this.createConnection(socket, frontEndId);
this.handleSocketDisconnect(socket, channel, frontEndId);
channelCreatedHandler(channel);
socket.emit(connection_management_1.ConnectionManagementMessages.INITIAL_CONNECT);
};
reconnectListener = (frontEndId) => {
socket.off(connection_management_1.ConnectionManagementMessages.INITIAL_CONNECT, initialConnectListener);
socket.off(connection_management_1.ConnectionManagementMessages.RECONNECT, reconnectListener);
const channel = this.connectionsByFrontend.get(frontEndId);
if (channel) {
console.info(`Reconnecting to front end ${frontEndId}`);
socket.emit(connection_management_1.ConnectionManagementMessages.RECONNECT, true);
channel.connect(socket);
this.handleSocketDisconnect(socket, channel, frontEndId);
const pendingTimeout = this.closeTimeouts.get(frontEndId);
clearTimeout(pendingTimeout);
this.closeTimeouts.delete(frontEndId);
}
else {
console.info(`Reconnecting failed for ${frontEndId}`);
socket.emit(connection_management_1.ConnectionManagementMessages.RECONNECT, false);
}
};
socket.on(connection_management_1.ConnectionManagementMessages.INITIAL_CONNECT, initialConnectListener);
socket.on(connection_management_1.ConnectionManagementMessages.RECONNECT, reconnectListener);
}
closeConnection(frontEndId, reason) {
console.info(`closing connection for ${frontEndId}`);
const connection = this.connectionsByFrontend.get(frontEndId); // not called when no connection is present
this.connectionsByFrontend.delete(frontEndId);
const pendingTimeout = this.closeTimeouts.get(frontEndId);
clearTimeout(pendingTimeout);
this.closeTimeouts.delete(frontEndId);
connection.onCloseEmitter.fire({ reason });
connection.close();
}
createConnection(socket, frontEndId) {
console.info(`creating connection for ${frontEndId}`);
const channel = new ReconnectableSocketChannel();
channel.connect(socket);
this.connectionsByFrontend.set(frontEndId, channel);
return channel;
}
handleSocketDisconnect(socket, channel, frontEndId) {
socket.on('disconnect', evt => {
console.info('socket closed');
channel.disconnect();
const timeout = this.frontendConnectionTimeout();
const isMarkedForClose = this.channelsMarkedForClose.delete(frontEndId);
if (timeout === 0 || isMarkedForClose) {
this.closeConnection(frontEndId, evt);
}
else if (timeout > 0) {
console.info(`setting close timeout for id ${frontEndId} to ${timeout}`);
const handle = setTimeout(() => {
this.closeConnection(frontEndId, evt);
}, timeout);
this.closeTimeouts.set(frontEndId, handle);
}
else {
// timeout < 0: never close the back end
}
});
}
markForClose(channelId) {
this.channelsMarkedForClose.add(channelId);
}
frontendConnectionTimeout() {
const envValue = Number(process.env['FRONTEND_CONNECTION_TIMEOUT']);
if (!isNaN(envValue)) {
return envValue;
}
return backend_application_config_provider_1.BackendApplicationConfigProvider.get().frontendConnectionTimeout;
}
};
exports.WebsocketFrontendConnectionService = WebsocketFrontendConnectionService;
tslib_1.__decorate([
(0, inversify_1.inject)(websocket_endpoint_1.WebsocketEndpoint),
tslib_1.__metadata("design:type", websocket_endpoint_1.WebsocketEndpoint)
], WebsocketFrontendConnectionService.prototype, "websocketServer", void 0);
exports.WebsocketFrontendConnectionService = WebsocketFrontendConnectionService = tslib_1.__decorate([
(0, inversify_1.injectable)()
], WebsocketFrontendConnectionService);
class ReconnectableSocketChannel extends channel_1.AbstractChannel {
constructor() {
super(...arguments);
this.socketBuffer = new socket_write_buffer_1.SocketWriteBuffer();
this.disposables = new common_1.DisposableCollection();
}
connect(socket) {
this.disposables = new common_1.DisposableCollection();
this.socket = socket;
const errorHandler = (err) => {
this.onErrorEmitter.fire(err);
};
this.disposables.push(common_1.Disposable.create(() => {
socket.off('error', errorHandler);
}));
socket.on('error', errorHandler);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dataListener = (data) => {
// In the browser context socketIO receives binary messages as ArrayBuffers.
// So we have to convert them to a Uint8Array before delegating the message to the read buffer.
const buffer = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
this.onMessageEmitter.fire(() => new uint8_array_message_buffer_1.Uint8ArrayReadBuffer(buffer));
};
this.disposables.push(common_1.Disposable.create(() => {
socket.off('message', dataListener);
}));
socket.on('message', dataListener);
this.socketBuffer.flush(socket);
}
disconnect() {
this.disposables.dispose();
this.socket = undefined;
}
getWriteBuffer() {
const writeBuffer = new uint8_array_message_buffer_1.Uint8ArrayWriteBuffer();
writeBuffer.onCommit(data => {
var _a;
if ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) {
this.socket.send(data);
}
else {
this.socketBuffer.buffer(data);
}
});
return writeBuffer;
}
}
//# sourceMappingURL=websocket-frontend-connection-service.js.map
;