util: Add promise-based stream wrapper

You'd think something like this would exist on npm somewhere.
This commit is contained in:
Tau 2020-03-11 17:23:44 -04:00 committed by da5669c09fdb0a288ba01e259a609d7779ac7fc9
parent 76583f17ce
commit fa5ed284e1
5 changed files with 161 additions and 0 deletions

4
src/util/stream/index.ts Normal file
View File

@ -0,0 +1,4 @@
import IoByteStream from "./stream";
export * from "./types";
export default IoByteStream;

78
src/util/stream/reader.ts Normal file
View File

@ -0,0 +1,78 @@
import { Readable } from "stream";
export default function makeReader(stm: Readable) {
let remain = Buffer.alloc(0);
let busy = false;
return function read(nbytes: number): Promise<Buffer> {
return new Promise((resolve, reject) => {
if (busy) {
return reject(new Error("Already reading"));
}
//
// Synchronous completion case
//
const result = Buffer.alloc(nbytes);
const step = Math.min(remain.length, nbytes);
remain.copy(result, 0, 0, step);
remain = remain.slice(step);
if (step === nbytes) {
return resolve(result);
}
//
// Asynchronous completion case
//
let pos = step;
let finish: () => void;
const onReadable = () => {
while (true) {
const chunk = stm.read() as (Buffer | null);
if (chunk === null) {
break;
}
const step = Math.min(result.length - pos, chunk.length);
chunk.copy(result, pos, 0, step);
remain = chunk.slice(step);
pos += step;
if (pos === result.length) {
finish();
resolve(result);
}
}
};
const onEnd = () => {
finish();
resolve(result.slice(0, pos));
};
const onError = error => {
finish();
reject(error);
};
finish = () => {
stm.off("end", onEnd);
stm.off("error", onError);
stm.off("readable", onReadable);
busy = false;
};
busy = true;
stm.on("end", onEnd);
stm.on("error", onError);
stm.on("readable", onReadable);
});
};
}

41
src/util/stream/stream.ts Normal file
View File

@ -0,0 +1,41 @@
import { Duplex } from "stream";
import makeReader from "./reader";
import { ByteStream } from "./types";
import makeWriter from "./writer";
export default class IoByteStream implements ByteStream {
private _stm?: Duplex;
private readonly _read: (nbytes: number) => Promise<Buffer>;
private readonly _write: (buf: Buffer) => Promise<void>;
constructor(stm: Duplex) {
this._stm = stm;
this._read = makeReader(stm);
this._write = makeWriter(stm);
}
close() {
if (this._stm !== undefined) {
this._stm.destroy();
}
this._stm = undefined;
}
read(nbytes: number): Promise<Buffer> {
if (this._stm === undefined) {
throw new Error("Stream is closed");
}
return this._read(nbytes);
}
write(buf: Buffer): Promise<void> {
if (this._stm === undefined) {
throw new Error("Stream is closed");
}
return this._write(buf);
}
}

13
src/util/stream/types.ts Normal file
View File

@ -0,0 +1,13 @@
export interface ReadableByteStream {
close(): void;
read(nbytes: number): Promise<Buffer>;
}
export interface WritableByteStream {
close(): void;
write(buf: Buffer): Promise<void>;
}
export type ByteStream = ReadableByteStream & WritableByteStream;

25
src/util/stream/writer.ts Normal file
View File

@ -0,0 +1,25 @@
import { Writable } from "stream";
export default function makeWriter(stm: Writable) {
let busy = false;
return function write(buf: Buffer): Promise<void> {
return new Promise((resolve, reject) => {
if (busy) {
return reject(new Error("Already writing"));
}
busy = true;
stm.write(buf, error => {
busy = false;
if (error !== undefined) {
reject(error);
} else {
resolve();
}
});
});
};
}