@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
152 lines • 7.56 kB
JavaScript
// *****************************************************************************
// Copyright (C) 2022 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-only WITH Classpath-exception-2.0
// *****************************************************************************
Object.defineProperty(exports, "__esModule", { value: true });
exports.BinaryMessagePipe = void 0;
const common_1 = require("../../common");
const uint8_array_message_buffer_1 = require("../../common/message-rpc/uint8-array-message-buffer");
/**
* A `BinaryMessagePipe` is capable of sending and retrieving binary messages i.e. {@link Uint8Array}s over
* and underlying streamed process pipe/fd. The message length of individual messages is encoding at the beginning of
* a new message. This makes it possible to extract messages from the streamed data.
*/
class BinaryMessagePipe {
constructor(underlyingPipe) {
this.underlyingPipe = underlyingPipe;
this.dataHandler = (chunk) => this.handleChunk(chunk);
this.onMessageEmitter = new common_1.Emitter();
this.cachedMessageData = {
chunks: [],
missingBytes: 0
};
underlyingPipe.on('data', this.dataHandler);
}
get onMessage() {
return this.onMessageEmitter.event;
}
send(message) {
this.underlyingPipe.write(this.encodeMessageStart(message));
this.underlyingPipe.write(message);
}
handleChunk(chunk) {
if (this.cachedMessageData.missingBytes === 0) {
// There is no currently streamed message => We expect that the beginning of the chunk is the message start for a new message
this.handleNewMessage(chunk);
}
else {
// The chunk contains message data intended for the currently cached message
this.handleMessageContentChunk(chunk);
}
}
handleNewMessage(chunk) {
if (chunk.byteLength < this.messageStartByteLength) {
// The chunk only contains a part of the encoded message start
this.cachedMessageData.partialMessageStart = chunk;
return;
}
const messageLength = this.readMessageStart(chunk);
if (chunk.length - this.messageStartByteLength > messageLength) {
// The initial chunk contains more than one binary message => Fire `onMessage` for first message and handle remaining content
const firstMessage = chunk.slice(this.messageStartByteLength, this.messageStartByteLength + messageLength);
this.onMessageEmitter.fire(firstMessage);
this.handleNewMessage(chunk.slice(this.messageStartByteLength + messageLength));
}
else if (chunk.length - this.messageStartByteLength === messageLength) {
// The initial chunk contains exactly one complete message. => Directly fire the `onMessage` event.
this.onMessageEmitter.fire(chunk.slice(this.messageStartByteLength));
}
else {
// The initial chunk contains only part of the message content => Cache message data
this.cachedMessageData.chunks = [chunk.slice(this.messageStartByteLength)];
this.cachedMessageData.missingBytes = messageLength - chunk.byteLength + this.messageStartByteLength;
}
}
handleMessageContentChunk(chunk) {
if (this.cachedMessageData) {
if (chunk.byteLength < this.cachedMessageData.missingBytes) {
// The chunk only contains parts of the missing bytes for the cached message.
this.cachedMessageData.chunks.push(chunk);
this.cachedMessageData.missingBytes -= chunk.byteLength;
}
else if (chunk.byteLength === this.cachedMessageData.missingBytes) {
// Chunk contains exactly the missing data for the cached message
this.cachedMessageData.chunks.push(chunk);
this.emitCachedMessage();
}
else {
// Chunk contains missing data for the cached message + data for the next message
const messageEnd = this.cachedMessageData.missingBytes;
const missingData = chunk.slice(0, messageEnd);
this.cachedMessageData.chunks.push(missingData);
this.emitCachedMessage();
this.handleNewMessage(chunk.slice(messageEnd));
}
}
}
emitCachedMessage() {
const message = Buffer.concat(this.cachedMessageData.chunks);
this.onMessageEmitter.fire(message);
this.cachedMessageData.chunks = [];
this.cachedMessageData.missingBytes = 0;
}
/**
* Encodes the start of a new message into a {@link Uint8Array}.
* The message start consists of a identifier string and the length of the following message.
* @returns the buffer contains the encoded message start
*/
encodeMessageStart(message) {
const writer = new uint8_array_message_buffer_1.Uint8ArrayWriteBuffer()
.writeString(BinaryMessagePipe.MESSAGE_START_IDENTIFIER)
.writeUint32(message.length);
const messageStart = writer.getCurrentContents();
writer.dispose();
return messageStart;
}
get messageStartByteLength() {
// 4 bytes for length of id + id string length + 4 bytes for length of message
return 4 + BinaryMessagePipe.MESSAGE_START_IDENTIFIER.length + 4;
}
/**
* Reads the start of a new message from a stream chunk (or cached message) received from the underlying pipe.
* The message start is expected to consist of an identifier string and the length of the message.
* @param chunk The stream chunk.
* @returns The length of the message content to read.
* @throws An error if the message start can not be read successfully.
*/
readMessageStart(chunk) {
const messageData = this.cachedMessageData.partialMessageStart ? Buffer.concat([this.cachedMessageData.partialMessageStart, chunk]) : chunk;
this.cachedMessageData.partialMessageStart = undefined;
const reader = new uint8_array_message_buffer_1.Uint8ArrayReadBuffer(messageData);
const identifier = reader.readString();
if (identifier !== BinaryMessagePipe.MESSAGE_START_IDENTIFIER) {
throw new Error(`Could not read message start. The start identifier should be '${BinaryMessagePipe.MESSAGE_START_IDENTIFIER}' but was '${identifier}`);
}
const length = reader.readUint32();
return length;
}
dispose() {
this.underlyingPipe.removeListener('data', this.dataHandler);
this.underlyingPipe.end();
this.onMessageEmitter.dispose();
this.cachedMessageData = {
chunks: [],
missingBytes: 0
};
}
}
exports.BinaryMessagePipe = BinaryMessagePipe;
BinaryMessagePipe.MESSAGE_START_IDENTIFIER = '<MessageStart>';
//# sourceMappingURL=binary-message-pipe.js.map
;