@chainsafe/libp2p-yamux
Version:
Yamux stream multiplexer for libp2p
130 lines • 4.95 kB
JavaScript
import { Uint8ArrayList } from 'uint8arraylist';
import { InvalidFrameError, InvalidStateError } from './errors.js';
import { FrameType, HEADER_LENGTH, YAMUX_VERSION } from './frame.js';
// used to bitshift in decoding
// native bitshift can overflow into a negative number, so we bitshift by multiplying by a power of 2
const twoPow24 = 2 ** 24;
/**
* Decode a header from the front of a buffer
*
* @param data - Assumed to have enough bytes for a header
*/
export function decodeHeader(data) {
if (data[0] !== YAMUX_VERSION) {
throw new InvalidFrameError('Invalid frame version');
}
return {
type: data[1],
flag: (data[2] << 8) + data[3],
streamID: (data[4] * twoPow24) + (data[5] << 16) + (data[6] << 8) + data[7],
length: (data[8] * twoPow24) + (data[9] << 16) + (data[10] << 8) + data[11]
};
}
/**
* Decodes yamux frames from a source
*/
export class Decoder {
source;
/** Buffer for in-progress frames */
buffer;
/** Used to sanity check against decoding while in an inconsistent state */
frameInProgress;
constructor(source) {
// Normally, when entering a for-await loop with an iterable/async iterable, the only ways to exit the loop are:
// 1. exhaust the iterable
// 2. throw an error - slow, undesirable if there's not actually an error
// 3. break or return - calls the iterable's `return` method, finalizing the iterable, no more iteration possible
//
// In this case, we want to enter (and exit) a for-await loop per chunked data frame and continue processing the iterable.
// To do this, we strip the `return` method from the iterator and can now `break` early and continue iterating.
// Exiting the main for-await is still possible via 1. and 2.
this.source = returnlessSource(source);
this.buffer = new Uint8ArrayList();
this.frameInProgress = false;
}
/**
* Emits frames from the decoder source.
*
* Note: If `readData` is emitted, it _must_ be called before the next iteration
* Otherwise an error is thrown
*/
async *emitFrames() {
for await (const chunk of this.source) {
this.buffer.append(chunk);
// Loop to consume as many bytes from the buffer as possible
// Eg: when a single chunk contains several frames
while (true) {
const header = this.readHeader();
if (header === undefined) {
break;
}
const { type, length } = header;
if (type === FrameType.Data) {
// This is a data frame, the frame body must still be read
// `readData` must be called before the next iteration here
this.frameInProgress = true;
yield {
header,
readData: this.readBytes.bind(this, length)
};
}
else {
yield { header };
}
}
}
}
readHeader() {
// Sanity check to ensure a header isn't read when another frame is partially decoded
// In practice this shouldn't happen
if (this.frameInProgress) {
throw new InvalidStateError('decoding frame already in progress');
}
if (this.buffer.length < HEADER_LENGTH) {
// not enough data yet
return;
}
const header = decodeHeader(this.buffer.subarray(0, HEADER_LENGTH));
this.buffer.consume(HEADER_LENGTH);
return header;
}
async readBytes(length) {
if (this.buffer.length < length) {
for await (const chunk of this.source) {
this.buffer.append(chunk);
if (this.buffer.length >= length) {
// see note above, the iterator is not `return`ed here
break;
}
}
}
const out = this.buffer.sublist(0, length);
this.buffer.consume(length);
// The next frame can now be decoded
this.frameInProgress = false;
return out;
}
}
/**
* Strip the `return` method from a `Source`
*/
export function returnlessSource(source) {
if (source[Symbol.iterator] !== undefined) {
const iterator = source[Symbol.iterator]();
iterator.return = undefined;
return {
[Symbol.iterator]() { return iterator; }
};
}
else if (source[Symbol.asyncIterator] !== undefined) {
const iterator = source[Symbol.asyncIterator]();
iterator.return = undefined;
return {
[Symbol.asyncIterator]() { return iterator; }
};
}
else {
throw new Error('a source must be either an iterable or an async iterable');
}
}
//# sourceMappingURL=decode.js.map