From fd70113b5b8f004ac9196ecd5743e34be4b14f37 Mon Sep 17 00:00:00 2001 From: Nina Doschek Date: Mon, 3 Jul 2023 11:08:25 +0200 Subject: [PATCH 1/2] GLSP-77: Allow WebSocket connections to reconnect after interrupt - Introduce GLSPWebSocketProvider to allow websocket connections to reconnect after interrupt - Make use of ws provider in standalone example Part of https://github.com/eclipse-glsp/glsp/issues/77 --- examples/workflow-standalone/src/app.ts | 45 +++++-- .../jsonrpc/ws-connection-provider.ts | 112 ++++++++++++++++++ packages/protocol/src/index.ts | 1 + 3 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 packages/protocol/src/client-server-protocol/jsonrpc/ws-connection-provider.ts diff --git a/examples/workflow-standalone/src/app.ts b/examples/workflow-standalone/src/app.ts index b72a3074..c65b4e49 100644 --- a/examples/workflow-standalone/src/app.ts +++ b/examples/workflow-standalone/src/app.ts @@ -18,15 +18,17 @@ import 'reflect-metadata'; import { ApplicationIdProvider, BaseJsonrpcGLSPClient, - configureServerActions, EnableToolPaletteAction, GLSPActionDispatcher, GLSPClient, GLSPDiagramServer, - listen, + GLSPWebSocketProvider, RequestModelAction, RequestTypeHintsAction, - TYPES + ServerMessageAction, + ServerStatusAction, + TYPES, + configureServerActions } from '@eclipse-glsp/client'; import { join, resolve } from 'path'; import { MessageConnection } from 'vscode-jsonrpc'; @@ -34,20 +36,22 @@ import createContainer from './di.config'; const port = 8081; const id = 'workflow'; const diagramType = 'workflow-diagram'; -const websocket = new WebSocket(`ws://localhost:${port}/${id}`); const loc = window.location.pathname; const currentDir = loc.substring(0, loc.lastIndexOf('/')); const examplePath = resolve(join(currentDir, '../app/example1.wf')); const clientId = ApplicationIdProvider.get() + '_' + examplePath; -const container = createContainer(); -const diagramServer = container.get(TYPES.ModelSource); +const webSocketUrl = `ws://localhost:${port}/${id}`; + +let container = createContainer(); +let diagramServer = container.get(TYPES.ModelSource); diagramServer.clientId = clientId; -listen(websocket, connection => initialize(connection)); +const wsProvider = new GLSPWebSocketProvider(webSocketUrl); +wsProvider.listen({ onConnection: initialize, onReconnect: reconnect, logger: console }); -async function initialize(connectionProvider: MessageConnection): Promise { +async function initialize(connectionProvider: MessageConnection, isReconnecting = false): Promise { const client = new BaseJsonrpcGLSPClient({ id, connectionProvider }); await diagramServer.connect(client); @@ -57,20 +61,39 @@ async function initialize(connectionProvider: MessageConnection): Promise }); await configureServerActions(result, diagramType, container); - const actionDispatcher = container.get(GLSPActionDispatcher); + const actionDispatcher: GLSPActionDispatcher = container.get(GLSPActionDispatcher); await client.initializeClientSession({ clientSessionId: diagramServer.clientId, diagramType }); + actionDispatcher.dispatch( RequestModelAction.create({ options: { sourceUri: `file://${examplePath}`, - diagramType + diagramType, + isReconnecting } }) ); actionDispatcher.dispatch(RequestTypeHintsAction.create()); await actionDispatcher.onceModelInitialized(); actionDispatcher.dispatch(EnableToolPaletteAction.create()); + + if (isReconnecting) { + const message = `Connection to the ${id} glsp server got closed. Connection was successfully re-established.`; + const timeout = 5000; + const severity = 'WARNING'; + actionDispatcher.dispatchAll([ + ServerStatusAction.create(message, { severity, timeout }), + ServerMessageAction.create(message, { severity }) + ]); + return; + } } -websocket.onerror = ev => alert('Connection to server errored. Please make sure that the server is running'); +async function reconnect(connectionProvider: MessageConnection): Promise { + container = createContainer(); + diagramServer = container.get(TYPES.ModelSource); + diagramServer.clientId = clientId; + + initialize(connectionProvider, true /* isReconnecting */); +} diff --git a/packages/protocol/src/client-server-protocol/jsonrpc/ws-connection-provider.ts b/packages/protocol/src/client-server-protocol/jsonrpc/ws-connection-provider.ts new file mode 100644 index 00000000..3e85cdcb --- /dev/null +++ b/packages/protocol/src/client-server-protocol/jsonrpc/ws-connection-provider.ts @@ -0,0 +1,112 @@ +/******************************************************************************** + * Copyright (c) 2023 EclipseSource 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { Logger, MessageConnection } from 'vscode-jsonrpc'; +import { MaybePromise } from '../../utils/type-util'; +import { createWebSocketConnection, wrap } from './websocket-connection'; + +export interface GLSPWebSocketOptions { + /** + * Allow automatic reconnect of WebSocket connections + * @default true + */ + reconnecting?: boolean; + /** + * Max attempts of reconnects + * @default Infinity + */ + reconnectAttempts?: number; + /** + * The time delay in milliseconds between reconnect attempts + * @default 1000 + */ + reconnectDelay?: number; +} + +export const GLSPConnectionHandler = Symbol('GLSPConnectionHandler'); +export interface GLSPConnectionHandler { + onConnection?(connection: MessageConnection): MaybePromise; + onReconnect?(connection: MessageConnection): MaybePromise; + logger?: Logger; +} + +export class GLSPWebSocketProvider { + protected webSocket: WebSocket; + protected reconnectTimer: NodeJS.Timer; + protected reconnectAttempts = 0; + + protected options: GLSPWebSocketOptions = { + // default values + reconnecting: true, + reconnectAttempts: Infinity, + reconnectDelay: 1000 + }; + + constructor(protected url: string, options?: GLSPWebSocketOptions) { + this.options = Object.assign(this.options, options); + } + + protected createWebSocket(url: string): WebSocket { + return new WebSocket(url); + } + + listen(handler: GLSPConnectionHandler, isReconnecting = false): Promise { + this.webSocket = this.createWebSocket(this.url); + + this.webSocket.onerror = (): void => { + handler.logger?.error('GLSPWebSocketProvider Connection to server errored. Please make sure that the server is running!'); + clearInterval(this.reconnectTimer); + this.webSocket.close(); + }; + + return new Promise(resolve => { + this.webSocket.onopen = (): void => { + clearInterval(this.reconnectTimer); + const wrappedSocket = wrap(this.webSocket); + const wsConnection = createWebSocketConnection(wrappedSocket, handler.logger); + + this.webSocket.onclose = (): void => { + const { reconnecting } = { ...this.options }; + if (reconnecting) { + if (this.reconnectAttempts >= this.options.reconnectAttempts!) { + handler.logger?.error( + 'GLSPWebSocketProvider WebSocket reconnect failed - maximum number reconnect attempts ' + + `(${this.options.reconnectAttempts}) was exceeded!` + ); + } else { + this.reconnectTimer = setInterval(() => { + handler.logger?.warn('GLSPWebSocketProvider reconnecting...'); + this.listen(handler, true); + this.reconnectAttempts++; + }, this.options.reconnectDelay!); + } + } else { + handler.logger?.error('GLSPWebSocketProvider WebSocket will not reconnect - closing the connection now!'); + } + }; + + if (isReconnecting) { + handler.logger?.warn('GLSPWebSocketProvider Reconnecting!'); + handler.onReconnect && handler.onReconnect(wsConnection); + } else { + handler.logger?.warn('GLSPWebSocketProvider Initializing!'); + handler.onConnection && handler.onConnection(wsConnection); + } + resolve(wsConnection); + }; + }); + } +} diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index 09b4b12a..0d7e65a0 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -54,6 +54,7 @@ export * from './client-server-protocol/jsonrpc/base-jsonrpc-glsp-client'; export * from './client-server-protocol/jsonrpc/glsp-jsonrpc-client'; export * from './client-server-protocol/jsonrpc/glsp-jsonrpc-server'; export * from './client-server-protocol/jsonrpc/websocket-connection'; +export * from './client-server-protocol/jsonrpc/ws-connection-provider'; export * from './client-server-protocol/types'; export * from './model/default-types'; export * from './model/model-schema'; From 7458644a80c9e71cfc1a01eae9f92164ab6563ac Mon Sep 17 00:00:00 2001 From: Nina Doschek Date: Mon, 24 Jul 2023 08:51:59 +0200 Subject: [PATCH 2/2] Address review comments --- .../jsonrpc/ws-connection-provider.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/protocol/src/client-server-protocol/jsonrpc/ws-connection-provider.ts b/packages/protocol/src/client-server-protocol/jsonrpc/ws-connection-provider.ts index 3e85cdcb..55ad9600 100644 --- a/packages/protocol/src/client-server-protocol/jsonrpc/ws-connection-provider.ts +++ b/packages/protocol/src/client-server-protocol/jsonrpc/ws-connection-provider.ts @@ -79,19 +79,19 @@ export class GLSPWebSocketProvider { const wsConnection = createWebSocketConnection(wrappedSocket, handler.logger); this.webSocket.onclose = (): void => { - const { reconnecting } = { ...this.options }; + const { reconnecting, reconnectAttempts, reconnectDelay } = this.options; if (reconnecting) { - if (this.reconnectAttempts >= this.options.reconnectAttempts!) { + if (this.reconnectAttempts >= reconnectAttempts!) { handler.logger?.error( 'GLSPWebSocketProvider WebSocket reconnect failed - maximum number reconnect attempts ' + - `(${this.options.reconnectAttempts}) was exceeded!` + `(${reconnectAttempts}) was exceeded!` ); } else { this.reconnectTimer = setInterval(() => { handler.logger?.warn('GLSPWebSocketProvider reconnecting...'); this.listen(handler, true); this.reconnectAttempts++; - }, this.options.reconnectDelay!); + }, reconnectDelay!); } } else { handler.logger?.error('GLSPWebSocketProvider WebSocket will not reconnect - closing the connection now!'); @@ -100,10 +100,10 @@ export class GLSPWebSocketProvider { if (isReconnecting) { handler.logger?.warn('GLSPWebSocketProvider Reconnecting!'); - handler.onReconnect && handler.onReconnect(wsConnection); + handler.onReconnect?.(wsConnection); } else { handler.logger?.warn('GLSPWebSocketProvider Initializing!'); - handler.onConnection && handler.onConnection(wsConnection); + handler.onConnection?.(wsConnection); } resolve(wsConnection); };