UNPKG

ts-stream

Version:

Type-safe object streams with seamless support for backpressure, ending, and error handling

212 lines (199 loc) 6.44 kB
/** * Promise-based object stream with seamless support for back-pressure and error * handling, written in Typescript. * * Copyright (C) 2015 Martin Poelstra * License: MIT */ import * as fs from "fs"; import * as NodeStream from "stream"; import { Readable, Stream } from "./Stream"; import { defer, swallowErrors, VoidDeferred } from "./util"; /** * Convert ts-stream into a Node.JS Readable instance. * * Usage example: * let sink = fs.createWriteStream("test.txt"); * let tsSource = Stream.from(["abc", "def"]); * let source = new NodeReadable(tsSource); * source.pipe(sink); * sink.on("error", (error: Error) => tsSource.abort(error)); * source.on("error", (error: Error) => { something like sink.destroy(); }); * * @see `pipeToNodeStream()` for easier error and completion handling. */ export class NodeReadable<T> extends NodeStream.Readable { private _resumer?: (value?: void | PromiseLike<void>) => void; /** * Create new NodeJS Readable based on given ts-stream Readable. * * @see class description for usage example * * @param tsReadable Source stream * @param options Optional options to pass to Node.JS Readable constructor */ constructor(tsReadable: Readable<T>, options?: NodeStream.ReadableOptions) { super(options); swallowErrors( tsReadable.forEach( (chunk: any): void | Promise<void> => { // Try to push data, returns true if there's still space if (this.push(chunk)) { return; } // Stream blocked, wait until _read() is called const d = defer(); this._resumer = d.resolve; return d.promise; }, (err?: Error) => { if (err) { this.emit("error", err); } this.push(null); // tslint:disable-line:no-null-keyword }, (abortError: Error) => { // Abort pending read, if necessary if (this._resumer) { this._resumer(Promise.reject(abortError)); this._resumer = undefined; } } ) ); } public _read(size: number): void { if (this._resumer) { this._resumer(); this._resumer = undefined; } } } /** * Convenience wrapper around Node's file stream. * * Usage example: * let source = Stream.from(["abc", "def"]); * source.pipe(new FileSink("test.txt")); * * To wait for the stream's result, use e.g. * let sink = source.pipe(new FileSink("test.txt")); * sink.result().then(() => console.log("ok"), (err) => console.log("error", err)); */ export class FileSink extends Stream<string> { /** * Construct writable ts-stream which writes all values to given file. * If the stream is ended with an error, the file is closed (and `result()`) * reflects that error. * * @see class description for usage example * * @param path Filename to wite to * @param options Optional options (see https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options) */ constructor( path: string, options?: | string | { flags?: string; encoding?: BufferEncoding; fd?: number; mode?: number; autoClose?: boolean; emitClose?: boolean; start?: number; highWaterMark?: number; } ) { super(); pipeToNodeStream(this, fs.createWriteStream(path, options)); } } /** * Pipe a ts-stream ReadableStream to a Node.JS WritableStream. * * Reads all values from `tsReadable` and writes them to `nodeWritable`. * When readable ends, writable is also ended. * * If an error occurs on the writable stream, the readable stream is aborted * with that error. * If the readable stream is ended with an error, that error is optionally * emitted on the writable stream, and then the writable stream is ended. * * Usage example: * let sink = fs.createWriteStream("test.txt"); * let source = Stream.from(["abc", "def"]); * let result = pipeToNodeStream(source, sink); * result.then(() => console.log("done"), (err) => console.log(err)); * * @see `NodeReadable` if you need an instance of a Node.JS ReadableStream * * @param tsReadable Source stream * @param nodeWritable Destination stream * @param emitError Whether to emit errors in tsReadable on nodeWritable * (default false). Useful for e.g. destroying a socket * when an error occurs. * @return Promise that resolves when stream is finished (rejected when an error * occurred) */ export function pipeToNodeStream<T>( tsReadable: Readable<T>, nodeWritable: NodeJS.WritableStream, emitError: boolean = false ): Promise<void> { const endDeferred = defer(); let blockedDeferred: VoidDeferred | undefined; // Handle errors emitted by node stream: abort ts-stream function handleNodeStreamError(error: Error): void { // Don't 're-emit' the same error on which we were triggered emitError = false; // Make sure stream's end result reflects error endDeferred.reject(error); tsReadable.abort(error); if (blockedDeferred) { blockedDeferred.reject(error); nodeWritable.removeListener("drain", blockedDeferred.resolve); } } // Optionally pass ts-stream errors to node stream function handleTsStreamError(error: Error): void { if (!emitError) { return; } emitError = false; nodeWritable.removeListener("error", handleNodeStreamError); // prevent abort nodeWritable.emit("error", error); } nodeWritable.once("error", handleNodeStreamError); nodeWritable.once("finish", () => { nodeWritable.removeListener("error", handleNodeStreamError); endDeferred.resolve(); // ignored if error happens earlier }); return tsReadable.forEach( (chunk: any): void | Promise<void> => { blockedDeferred = undefined; // Try to push data, returns true if there's still space const canAcceptMore = nodeWritable.write(chunk); if (!canAcceptMore) { // Stream blocked, wait until drain is emitted blockedDeferred = defer(); nodeWritable.once("drain", blockedDeferred.resolve); return blockedDeferred.promise; } }, (endError?: Error): Promise<void> => { if (endError) { handleTsStreamError(endError); } // Note: we don't pass a callback to end(), as some types of // streams (e.g. sockets) may forget to call the callback in // some scenarios (e.g. connection reset). // We use the "finish" event to mark the stream's end, but it // doesn't get emitted on e.g. a connection refused error. nodeWritable.end(); return endDeferred.promise; }, handleTsStreamError // abort handler ); }