diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d88d69f..cf6b00e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,16 @@ - [layout] Improve Layouter to support more dynamic layouts and complex parent/children node structures [#187](https://github.com/eclipse-glsp/glsp-client/pull/187) - Contributed on behalf of STMicroelectronics - [diagram] Fixed SVG export for nested root elements e.g. `GLSPProjectionView` [#196](https://github.com/eclipse-glsp/glsp-client/pull/196) - [diagram] scope the styles to not break existing application layout [#209](https://github.com/eclipse-glsp/glsp-client/pull/209) -- [routing] Ensured that routes are properly re-calculated when moving a routing point [#198](https://github.com/eclipse-glsp/glsp-client/pull/198) +- [routing] Ensured that routes are properly re-calculated when moving a routing point [#198](https://github.com/eclipse-glsp/glsp-client/pull/198) - [diagram] Fixed a bug in the `EditLabelUIExtension` where the diagram becomes dirt wihtout an acual change. [#766](https://github.com/eclipse-glsp/glsp/issues/766) -- [diagram] Extends `ComputedBoundsAction` definition with routing information. This enables proper forwarding of cliend-side computed routes to the server. [#201](https://github.com/eclipse-glsp/glsp-client/pull/201/) +- [diagram] Extends `ComputedBoundsAction` definition with routing information. This enables proper forwarding of client-side computed routes to the server. [#201](https://github.com/eclipse-glsp/glsp-client/pull/201/) ### Breaking Changes - [DI] Injecting an `IButtonHandler` constructor is now deprecated. Please use `configureButtonHandler()` instead. [#195](https://github.com/eclipse-glsp/glsp-client/pull/195) - Contributed on behalf of STMicroelectronics -- [protocol] Update vscode-ws-jsonrpc to version 2.0.1 [#210](https://github.com/eclipse-glsp/glsp-client/pull/210) - [node] Update minimum requirements for Node to >=16.11.0 [#210](https://github.com/eclipse-glsp/glsp-client/pull/210) - [protocol] Renamed `UndoOperation` and `RedoOperation` to `UndoAction` and `RedoAction` to match operation specification [#216](https://github.com/eclipse-glsp/glsp-client/pull/216) +- [protocol] Remove dependency to `vscode-ws-jsonrpc`. The protocol package now directly offers functions to create a websocket rpc connections [#215](https://github.com/eclipse-glsp/glsp-client/pull/215) ## [v1.0.0 - 30/06/2022](https://github.com/eclipse-glsp/glsp-client/releases/tag/v1.0.0) diff --git a/examples/workflow-standalone/src/app.ts b/examples/workflow-standalone/src/app.ts index af9e10b0..5f681c7d 100644 --- a/examples/workflow-standalone/src/app.ts +++ b/examples/workflow-standalone/src/app.ts @@ -21,6 +21,7 @@ import { GLSPActionDispatcher, GLSPClient, GLSPDiagramServer, + listen, RequestModelAction, RequestTypeHintsAction, TYPES @@ -42,9 +43,7 @@ const container = createContainer(); const diagramServer = container.get(TYPES.ModelSource); diagramServer.clientId = clientId; -import('vscode-ws-jsonrpc').then(jsonrpc => { - jsonrpc.listen({ webSocket: websocket, onConnection: connection => initialize(connection) }); -}); +listen(websocket, connection => initialize(connection)); async function initialize(connectionProvider: MessageConnection): Promise { const client = new BaseJsonrpcGLSPClient({ id, connectionProvider }); diff --git a/packages/protocol/package.json b/packages/protocol/package.json index c48fc391..f9102769 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -48,7 +48,7 @@ "dependencies": { "sprotty-protocol": "0.13.0-next.f4445dd.342", "uuid": "7.0.3", - "vscode-ws-jsonrpc": "^2.0.1" + "vscode-jsonrpc": "^8.0.2" }, "devDependencies": { "@types/uuid": "3.4.5" diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index 4e9b50eb..f8b3226c 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -50,6 +50,7 @@ export * from './action-protocol'; export * from './glsp-client'; export * from './jsonrpc/base-jsonrpc-glsp-client'; export * from './jsonrpc/glsp-jsonrpc-client'; +export * from './jsonrpc/websocket-connection'; export * from './model/default-types'; export * from './model/model-schema'; export * from './utils/array-util'; diff --git a/packages/protocol/src/jsonrpc/websocket-connection.ts b/packages/protocol/src/jsonrpc/websocket-connection.ts new file mode 100644 index 00000000..cb037e54 --- /dev/null +++ b/packages/protocol/src/jsonrpc/websocket-connection.ts @@ -0,0 +1,180 @@ +/******************************************************************************** + * 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +// based on https://github.com/TypeFox/monaco-languageclient/blob/vwj-2.0.1/packages/vscode-ws-jsonrpc/src/socket/reader.ts +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2018-2022 TypeFox GmbH (http://www.typefox.io). All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +import { + AbstractMessageReader, + AbstractMessageWriter, + createMessageConnection, + DataCallback, + Disposable, + Logger, + Message, + MessageConnection +} from 'vscode-jsonrpc'; + +/** + * A wrapper interface that enables the reuse of the {@link WebSocketMessageReader} and {@link WebSocketMessageWriter} + * independent of the underlying WebSocket implementation/library. e.g. one could use Socket.io instead of plain WebSockets + */ +export interface WebSocketWrapper extends Disposable { + send(content: string): void; + onMessage(cb: (data: any) => void): void; + onError(cb: (reason: any) => void): void; + onClose(cb: (code: number, reason: string) => void): void; +} + +/** + * Creates a {@link WebSocketWrapper} for the given plain WebSocket + * @param socket The socket to wrap + */ +export function wrap(socket: WebSocket): WebSocketWrapper { + return { + send: content => socket.send(content), + onMessage: cb => (socket.onmessage = event => cb(event.data)), + onClose: cb => (socket.onclose = event => cb(event.code, event.reason)), + onError: cb => + (socket.onerror = event => { + if ('error' in event) { + cb(event.error); + } + }), + dispose: () => socket.close() + }; +} + +/** + * A `vscode-jsonrpc` {@link MessageReader} that reads messages from an underlying {@link WebSocketWrapper}. + */ +export class WebSocketMessageReader extends AbstractMessageReader { + protected state: 'initial' | 'listening' | 'closed' = 'initial'; + protected callback?: DataCallback; + protected eventQueue: Array<{ message?: unknown; error?: any }> = []; + + constructor(protected readonly socket: WebSocketWrapper) { + super(); + this.socket.onMessage(message => this.handleMessage(message)); + this.socket.onError(error => this.fireError(error)); + this.socket.onClose(() => this.fireClose()); + } + + listen(callback: DataCallback): Disposable { + if (this.state === 'initial') { + this.state = 'listening'; + this.callback = callback; + this.eventQueue.forEach(event => { + if (event.message) { + this.handleMessage(event.message); + } else if (event.error) { + this.fireError(event.error); + } else { + this.fireClose(); + } + }); + this.eventQueue = []; + } + return Disposable.create(() => { + this.callback = undefined; + this.eventQueue = []; + }); + } + + protected handleMessage(message: any): void { + if (this.state === 'initial') { + this.eventQueue.push({ message }); + } else if (this.state === 'listening') { + const data = JSON.parse(message); + this.callback!(data); + } + } + + protected override fireError(error: any): void { + if (this.state === 'initial') { + this.eventQueue.push({ error }); + } else if (this.state === 'listening') { + super.fireError(error); + } + } + + protected override fireClose(): void { + if (this.state === 'initial') { + this.eventQueue.push({}); + } else if (this.state === 'listening') { + super.fireClose(); + } + this.state = 'closed'; + } +} + +/** + * A `vscode-jsonrpc` {@link MessageReader} that writes messages to an underlying {@link WebSocketWrapper}. + */ +export class WebSocketMessageWriter extends AbstractMessageWriter { + protected errorCount = 0; + + constructor(protected readonly socket: WebSocketWrapper) { + super(); + } + + end(): void { + /** no-op */ + } + + async write(msg: Message): Promise { + try { + const content = JSON.stringify(msg); + this.socket.send(content); + } catch (e) { + this.errorCount++; + this.fireError(e, msg, this.errorCount); + } + } +} + +/** + * Create a `vscode-jsonrpc` {@link MessageConnection} on top of a given {@link WebSocketWrapper}. + */ +export function createWebSocketConnection(socket: WebSocketWrapper, logger?: Logger): MessageConnection { + const reader = new WebSocketMessageReader(socket); + const writer = new WebSocketMessageWriter(socket); + return createMessageConnection(reader, writer, logger); +} + +/** + * Creates a new {@link MessageConnection} on top of the given websocket on open. + * @param webSocket The target webSocket + * @param onConnection Optional callback that is invoked after the connection has been created + * @param logger Optional connection logger + * @returns A promise of the created connection + */ +export function listen( + webSocket: WebSocket, + onConnection?: (connection: MessageConnection) => void, + logger?: Logger +): Promise { + return new Promise(resolve => { + webSocket.onopen = () => { + const socket = wrap(webSocket); + const connection = createWebSocketConnection(socket, logger); + onConnection?.(connection); + resolve(connection); + }; + }); +} diff --git a/packages/protocol/src/utils/type-util.ts b/packages/protocol/src/utils/type-util.ts index a36fa18c..8febd827 100644 --- a/packages/protocol/src/utils/type-util.ts +++ b/packages/protocol/src/utils/type-util.ts @@ -61,7 +61,7 @@ export type TypeGuard = (element: any) => element is T; /** * Utility function that create a typeguard function for a given class constructor. * Essentially this wraps an instance of check as typeguard function. - * @param constructor The constructor fo the class for which the typeguard should be created. + * @param constructor The constructor of the class for which the typeguard should be created. * @returns The typeguard for this class. */ export function toTypeGuard(constructor: Constructor): TypeGuard { @@ -69,7 +69,7 @@ export function toTypeGuard(constructor: Constructor): TypeGuard { } /** - * Validates whether the given object as a property of type `string` with the given key. + * Validates whether the given object has a property of type `string` with the given key. * @param object The object that should be validated * @param propertyKey The key of the property * @param optional Flag to indicate wether the property can be optional i.e. also return true if the given key is undefined @@ -80,7 +80,7 @@ export function hasStringProp(object: AnyObject, propertyKey: string, optional = } /** - * Validates whether the given object as a property of type `boolean` with the given key. + * Validates whether the given object has a property of type `boolean` with the given key. * @param object The object that should be validated * @param propertyKey The key of the property * @param optional Flag to indicate wether the property can be optional i.e. also return true if the given key is undefined @@ -91,7 +91,7 @@ export function hasBooleanProp(object: AnyObject, propertyKey: string, optional } /** - * Validates whether the given object as a property of type `number` with the given key. + * Validates whether the given object has a property of type `number` with the given key. * @param object The object that should be validated * @param propertyKey The key of the property * @param optional Flag to indicate wether the property can be optional i.e. also return true if the given key is undefined @@ -102,7 +102,7 @@ export function hasNumberProp(object: AnyObject, propertyKey: string, optional = } /** - * Validates whether the given object as a property of type `object` with the given key. + * Validates whether the given object has a property of type `object` with the given key. * @param object The object that should be validated * @param propertyKey The key of the property * @param optional Flag to indicate wether the property can be optional i.e. also return true if the given key is undefined @@ -113,7 +113,7 @@ export function hasObjectProp(object: AnyObject, propertyKey: string, optional = } /** - * Validates whether the given object as a property of type `function` with the given key. + * Validates whether the given object has a property of type `function` with the given key. * @param object The object that should be validated * @param propertyKey The key of the property * @param optional Flag to indicate wether the property can be optional i.e. also return true if the given key is undefined @@ -124,7 +124,7 @@ export function hasFunctionProp(object: AnyObject, propertyKey: string, optional } /** - * Validates whether the given object as a property of type `Array` with the given key. + * Validates whether the given object has a property of type `Array` with the given key. * @param object The object that should be validated * @param propertyKey The key of the property * @param optional Flag to indicate wether the property can be optional i.e. also return true if the given key is undefined diff --git a/yarn.lock b/yarn.lock index 2c86af19..70dd2438 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6904,18 +6904,11 @@ validate-npm-package-name@^4.0.0: dependencies: builtins "^5.0.0" -vscode-jsonrpc@8.0.2: +vscode-jsonrpc@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz#f239ed2cd6004021b6550af9fd9d3e47eee3cac9" integrity sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ== -vscode-ws-jsonrpc@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-2.0.1.tgz#e0ac448a4a1df57d982a5abc62fc7128f4972587" - integrity sha512-ne5DO8/qe8tHt1U4LafLiYS832Yd4OltkP4+YZVOQwqGEU5nwLwZowUBqqEWt8sOZ0eLdCLV9luotGC2aUQ+LA== - dependencies: - vscode-jsonrpc "8.0.2" - walk-up-path@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-1.0.0.tgz#d4745e893dd5fd0dbb58dd0a4c6a33d9c9fec53e"