UNPKG

@eclipse-glsp/protocol

Version:

The protocol definition for client-server communication in GLSP

181 lines (165 loc) 6.54 kB
/******************************************************************************** * 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 | ArrayBufferLike | ArrayBufferView): void; onMessage(cb: (data: unknown) => void): void; onError(cb: (reason: unknown) => 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?: unknown }> = []; 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: unknown): 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<void> { 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<MessageConnection> { return new Promise(resolve => { webSocket.onopen = () => { const socket = wrap(webSocket); const connection = createWebSocketConnection(socket, logger); onConnection?.(connection); resolve(connection); }; }); }