@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
297 lines (250 loc) • 9.83 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2021 Red Hat, Inc. 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
// *****************************************************************************
import { Disposable, DisposableCollection } from '../disposable';
import { Emitter, Event } from '../event';
import { ReadBuffer, WriteBuffer } from './message-buffer';
/**
* A channel is a bidirectional communications channel with lifecycle and
* error signalling. Note that creation of channels is specific to particular
* implementations and thus not part of the protocol.
*/
export interface Channel {
/**
* The remote side has closed the channel
*/
onClose: Event<ChannelCloseEvent>;
/**
* An error has occurred while writing to or reading from the channel
*/
onError: Event<unknown>;
/**
* A message has arrived and can be read by listeners using a {@link MessageProvider}.
*/
onMessage: Event<MessageProvider>;
/**
* Obtain a {@link WriteBuffer} to write a message to the channel.
*/
getWriteBuffer(): WriteBuffer;
/**
* Close this channel. No {@link onClose} event should be sent
*/
close(): void;
}
/**
* The event that is emitted when a channel is closed from the remote side.
*/
export interface ChannelCloseEvent {
reason: string,
code?: number
};
/**
* The `MessageProvider` is emitted when a channel receives a new message.
* Listeners can invoke the provider to obtain a new {@link ReadBuffer} for the received message.
* This ensures that each listener has its own isolated {@link ReadBuffer} instance.
*
*/
export type MessageProvider = () => ReadBuffer;
/**
* Reusable abstract {@link Channel} implementation that sets up
* the basic channel event listeners and offers a generic close method.
*/
export abstract class AbstractChannel implements Channel {
onCloseEmitter: Emitter<ChannelCloseEvent> = new Emitter();
get onClose(): Event<ChannelCloseEvent> {
return this.onCloseEmitter.event;
};
onErrorEmitter: Emitter<unknown> = new Emitter();
get onError(): Event<unknown> {
return this.onErrorEmitter.event;
};
onMessageEmitter: Emitter<MessageProvider> = new Emitter();
get onMessage(): Event<MessageProvider> {
return this.onMessageEmitter.event;
};
protected toDispose: DisposableCollection = new DisposableCollection();
constructor() {
this.toDispose.pushAll([this.onCloseEmitter, this.onErrorEmitter, this.onMessageEmitter]);
}
close(): void {
this.toDispose.dispose();
}
abstract getWriteBuffer(): WriteBuffer;
}
/**
* A very basic {@link AbstractChannel} implementation which takes a function
* for retrieving the {@link WriteBuffer} as constructor argument.
*/
export class BasicChannel extends AbstractChannel {
constructor(protected writeBufferProvider: () => WriteBuffer) {
super();
}
getWriteBuffer(): WriteBuffer {
return this.writeBufferProvider();
}
}
/**
* Helper class to implement the single channels on a {@link ChannelMultiplexer}. Simply forwards write requests to
* the given write buffer source i.e. the main channel of the {@link ChannelMultiplexer}.
*/
export class ForwardingChannel extends AbstractChannel {
constructor(readonly id: string, protected readonly closeHandler: () => void, protected readonly writeBufferSource: () => WriteBuffer) {
super();
}
getWriteBuffer(): WriteBuffer {
return this.writeBufferSource();
}
override close(): void {
super.close();
this.closeHandler();
}
}
/**
* The different message types used in the messaging protocol of the {@link ChannelMultiplexer}
*/
export enum MessageTypes {
Open = 1,
Close = 2,
AckOpen = 3,
Data = 4
}
/**
* The write buffers in this implementation immediately write to the underlying
* channel, so we rely on writers to the multiplexed channels to always commit their
* messages and always in one go.
*/
export class ChannelMultiplexer implements Disposable {
protected pendingOpen: Map<string, (channel: ForwardingChannel) => void> = new Map();
protected openChannels: Map<string, ForwardingChannel> = new Map();
protected readonly onOpenChannelEmitter = new Emitter<{ id: string, channel: Channel }>();
get onDidOpenChannel(): Event<{ id: string, channel: Channel }> {
return this.onOpenChannelEmitter.event;
}
protected toDispose = new DisposableCollection();
constructor(protected readonly underlyingChannel: Channel) {
this.toDispose.pushAll([
this.underlyingChannel.onMessage(buffer => this.handleMessage(buffer())),
this.underlyingChannel.onClose(event => this.onUnderlyingChannelClose(event)),
this.underlyingChannel.onError(error => this.handleError(error)),
this.onOpenChannelEmitter
]);
}
protected handleError(error: unknown): void {
this.openChannels.forEach(channel => {
channel.onErrorEmitter.fire(error);
});
}
onUnderlyingChannelClose(event?: ChannelCloseEvent): void {
if (!this.toDispose.disposed) {
this.toDispose.push(Disposable.create(() => {
this.pendingOpen.clear();
this.openChannels.forEach(channel => {
channel.onCloseEmitter.fire(event ?? { reason: 'Multiplexer main channel has been closed from the remote side!' });
});
this.openChannels.clear();
}));
this.dispose();
}
}
protected handleMessage(buffer: ReadBuffer): void {
const type = buffer.readUint8();
const id = buffer.readString();
switch (type) {
case MessageTypes.AckOpen: {
return this.handleAckOpen(id);
}
case MessageTypes.Open: {
return this.handleOpen(id);
}
case MessageTypes.Close: {
return this.handleClose(id);
}
case MessageTypes.Data: {
return this.handleData(id, buffer);
}
}
}
protected handleAckOpen(id: string): void {
// edge case: both side try to open a channel at the same time.
const resolve = this.pendingOpen.get(id);
if (resolve) {
const channel = this.createChannel(id);
this.pendingOpen.delete(id);
this.openChannels.set(id, channel);
resolve(channel);
this.onOpenChannelEmitter.fire({ id, channel });
}
}
protected handleOpen(id: string): void {
if (!this.openChannels.has(id)) {
const channel = this.createChannel(id);
this.openChannels.set(id, channel);
const resolve = this.pendingOpen.get(id);
if (resolve) {
// edge case: both side try to open a channel at the same time.
resolve(channel);
}
this.underlyingChannel.getWriteBuffer().writeUint8(MessageTypes.AckOpen).writeString(id).commit();
this.onOpenChannelEmitter.fire({ id, channel });
}
}
protected handleClose(id: string): void {
const channel = this.openChannels.get(id);
if (channel) {
channel.onCloseEmitter.fire({ reason: 'Channel has been closed from the remote side' });
this.openChannels.delete(id);
}
}
protected handleData(id: string, data: ReadBuffer): void {
const channel = this.openChannels.get(id);
if (channel) {
channel.onMessageEmitter.fire(() => data.sliceAtReadPosition());
}
}
protected createChannel(id: string): ForwardingChannel {
return new ForwardingChannel(id, () => this.closeChannel(id), () => this.prepareWriteBuffer(id));
}
// Prepare the write buffer for the channel with the give, id. The channel id has to be encoded
// and written to the buffer before the actual message.
protected prepareWriteBuffer(id: string): WriteBuffer {
const underlying = this.underlyingChannel.getWriteBuffer();
underlying.writeUint8(MessageTypes.Data);
underlying.writeString(id);
return underlying;
}
protected closeChannel(id: string): void {
this.underlyingChannel.getWriteBuffer()
.writeUint8(MessageTypes.Close)
.writeString(id)
.commit();
this.openChannels.delete(id);
}
open(id: string): Promise<Channel> {
if (this.openChannels.has(id)) {
throw new Error(`Another channel with the id '${id}' is already open.`);
}
const result = new Promise<Channel>((resolve, reject) => {
this.pendingOpen.set(id, resolve);
});
this.underlyingChannel.getWriteBuffer().writeUint8(MessageTypes.Open).writeString(id).commit();
return result;
}
getOpenChannel(id: string): Channel | undefined {
return this.openChannels.get(id);
}
dispose(): void {
this.toDispose.dispose();
}
}