Skip to content

Commit

Permalink
GLSP-77: Allow WebSocket connections to reconnect after interrupt
Browse files Browse the repository at this point in the history
- Introduce GLSPWebSocketProvider to allow websocket connections to reconnect after interrupt
- Make use of ws provider in standalone example

Part of eclipse-glsp/glsp#77
  • Loading branch information
ndoschek committed Jul 20, 2023
1 parent 0363f0b commit fd70113
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 11 deletions.
45 changes: 34 additions & 11 deletions examples/workflow-standalone/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,40 @@ 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';
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<GLSPDiagramServer>(TYPES.ModelSource);
const webSocketUrl = `ws://localhost:${port}/${id}`;

let container = createContainer();
let diagramServer = container.get<GLSPDiagramServer>(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<void> {
async function initialize(connectionProvider: MessageConnection, isReconnecting = false): Promise<void> {
const client = new BaseJsonrpcGLSPClient({ id, connectionProvider });

await diagramServer.connect(client);
Expand All @@ -57,20 +61,39 @@ async function initialize(connectionProvider: MessageConnection): Promise<void>
});
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<void> {
container = createContainer();
diagramServer = container.get<GLSPDiagramServer>(TYPES.ModelSource);
diagramServer.clientId = clientId;

initialize(connectionProvider, true /* isReconnecting */);
}
Original file line number Diff line number Diff line change
@@ -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<void>;
onReconnect?(connection: MessageConnection): MaybePromise<void>;
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<MessageConnection> {
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);
};
});
}
}
1 change: 1 addition & 0 deletions packages/protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit fd70113

Please sign in to comment.