ts-stream
Version:
Type-safe object streams with seamless support for backpressure, ending, and error handling
1,347 lines (1,286 loc) • 58.3 kB
text/typescript
/**
* Promise-based object stream with seamless support for back-pressure and error
* handling, written in Typescript.
*
* Copyright (C) 2015 Martin Poelstra
* License: MIT
*/
/* istanbul ignore next */ // ignores Typescript's __extend() function
import BaseError from "./BaseError";
import { filter, map, Transform } from "./Transform";
import {
defer,
Deferred,
noop,
swallowErrors,
track,
TrackedPromise,
} from "./util";
/**
* Required methods for both readable and writable parts of a stream.
*/
export interface Common<T> {
/**
* Obtain a promise that resolves when all parts of a stream chain have
* completely ended.
*
* Specifically:
* - `end()` has been called (possibly with an Error),
* - `ender` callback has run and its returned promise resolved,
* - `end()`'s result parameter (if any) has been resolved.
*
* @return Promise resolved when stream chain has completely ended
*/
result(): Promise<void>;
/**
* Signal that the stream is aborted: it will no longer read incoming
* elements and will no longer write elements except in the course of
* processing any pending asynchronous reader callbacks (i.e. unresolved
* Promises returned by `forEach()` or other stream iterators). Does not
* end the stream.
*
* An upstream source can handle this `abort()` by catching the exception
* from its own `aborted()` method--for example, to cancel pending fetch
* operations, or close a continuous data stream.
*
* If the stream's `forEach()` function provided an `aborter` callback and
* the stream is not yet ended, `aborter` will be called with the abort reason.
* This can be used to cancel any remaining operations inside the asynchronous
* reader callback.
*
* Once the last pending callback is resolved, any pending and future `write()`s
* to this stream will be rejected with the error provided to `abort()`.
*
* It is still necessary to explicitly `end()` the stream, to ensure that any
* resources can be cleaned up correctly both on the reader and writer side.
* The stream's `ender` callback will be called with the abort error (i.e. any
* error passed to `end()` is ignored.)
*
* The abort is ignored if the stream is already aborted.
*
* It's possible to abort an ended stream. This can be used to 'bubble' an
* abort signal to other parts in a chain of streams which may not have ended
* yet. It will not change the end-state of this part of the stream though.
*
* @param reason Optional Error value to signal a reason for the abort
*/
abort(reason?: Error): void;
/**
* Obtain promise that resolves to a rejection when `abort()` is called.
*
* Useful to pass abort to upstream sources.
*
* Note: this promise either stays pending, or is rejected. It is never
* fulfilled.
*
* @return Promise that is rejected with abort error when stream is aborted
*/
aborted(): Promise<never>;
/**
* Obtain promise that resolves to a rejection when `abort()` is called.
*
* Useful to pass abort to upstream sources.
*
* Note: this promise either stays pending, or is rejected. It is never
* fulfilled.
*
* @return Promise that is rejected with abort error when stream is aborted
*/
aborted(aborter: (reason: Error) => void): void;
}
/**
* Required methods for the readable part of a stream.
*/
export interface Readable<T> extends Common<T> {
/**
* Read all values from stream, optionally waiting for explicit stream end.
*
* `reader` is called for every written value.
*
* `ender` is called once, when the writer `end()`s the stream, either with
* or without an error.
*
* `reader` and `ender` callbacks can return a promise to indicate when the
* value or end-of-stream condition has been completely processed. This
* ensures that a fast writer can never overload a slow reader, and is
* called 'backpressure'.
*
* The `reader` callback is never called while its previously returned
* promise is still pending, and the `ender` callback is likewise only
* called after all reads have completed.
*
* The corresponding `write()` or `end()` operation is blocked until the
* value returned from the reader or ender callback is resolved. If the
* callback throws an error or the returned promise resolves to a rejection,
* the `write()` or `end()` will be rejected with it.
*
* All callbacks are always called asynchronously (i.e. some time after
* `forEach()`, `write()`, `end()` or `abort()` returns), and their `this`
* argument will be undefined.
*
* `aborter` is called once if the stream is aborted and has not ended yet.
* (I.e. it will be called if e.g. `ender`'s returned promise is still
* pending, to allow early termination, but it will no longer be called
* if its promise has resolved).
*
* The `aborter` callback can be called while a reader callback's promise is
* still pending, and should try to let `reader` or `ender` finish as fast
* as possible.
*
* Note that even when a stream is aborted, it still needs to be `end()`'ed
* correctly.
*
* If no `ender` is given, a default end handler is installed that directly
* acknowledges the end-of-stream, also in case of an error. Note that that
* error will still be returned from `forEach()`.
*
* If no `aborter` is given, an abort is ignored (but will still cause
* further writes to fail, and it will be reflected in the returned promise).
*
* It is an error to call `forEach()` multiple times, and it is not possible
* to 'detach' already attached callbacks. Reason is that the exact behaviour
* of such actions (e.g. block or simply ignore further writes) is application
* dependent, and should be implemented as a transform instead.
*
* The return value of `forEach()` is `result()`, a promise that resolves
* when all parts in the stream(-chain) have completely finished.
*
* @param reader Callback called with every written value
* @param ender Optional callback called when stream is ended
* @param aborter Optional callback called when stream is aborted
* @return Promise for completely finished stream, i.e. same promise as `result()`
*/
forEach(
reader: (value: T) => void | PromiseLike<void>,
ender?: (error?: Error) => void | PromiseLike<void>,
aborter?: (error: Error) => void
): Promise<void>;
}
/**
* Required methods for the writable part of a stream.
*/
export interface Writable<T> extends Common<T> {
/**
* Write value (or promise for value) to stream.
*
* Writer is blocked until the value is read by the read handler passed to
* `forEach()`, and the value returned by that read handler is resolved.
*
* It is an error to write an `undefined` value (as this is a common
* programming error). Writing a promise for a void is currently allowed,
* but discouraged.
*
* The promise returned by `write()` will be rejected with the same reason if:
* - the written value is a PromiseLike that resolves to a rejection
* - the read handler throws an error or returns a rejected promise
* It is still possible to write another value after that, or e.g. `end()`
* the stream with or without an error.
*
* @param value Value to write, or promise for it
* @return Void-promise that resolves when value was processed by reader
*/
write(value: T | PromiseLike<T>): Promise<void>;
/**
* End the stream, optionally passing an error.
*
* Already pending writes will be processed by the reader passed to
* `forEach()` before passing the end-of-stream to its end handler.
*
* The returned promise will resolve after the end handler has finished
* processing. It is rejected if the end handler throws an error or returns
* a rejected promise.
*
* All calls to `write()` or `end()` after the first `end()` will be
* rejected with a `WriteAfterEndError`.
*
* By default, this stream's `result()` will be resolved when `end()`
* resolves, or rejected with the error if `end()` is called with an error.
* It is possible to let this stream's `result()` 'wait' until any upstream
* streams have completed by e.g. passing that upstream's `result()` as the
* second argument to `end()`.
*
* Note: even if a stream is aborted, it is still necessary to call `end()`
* to allow any resources to correctly be cleaned up.
*
* @param error Optional Error to pass to `forEach()` end handler
* @param result Optional promise that determines final value of `result()`
* @return Void-promise that resolves when `ended`-handler has processed the
* end-of-stream
*/
end(error?: Error, result?: PromiseLike<void>): Promise<void>;
}
export interface CommonStream<T> {
/**
* Determine whether `end()` has been called on the stream, but the stream
* is still processing it.
*
* @return true when `end()` was called but not acknowledged yet, false
* otherwise
*/
isEnding(): boolean;
/**
* Determine whether stream has completely ended (i.e. end handler has been
* called and its return PromiseLike, if any, is resolved).
*
* @return true when stream has ended, false otherwise
*/
isEnded(): boolean;
/**
* Determine whether `end()` has been called on the stream.
*
* @return true when `end()` was called
*/
isEndingOrEnded(): boolean;
/**
* Determine whether `forEach()` callback(s) are currently attached to the
* stream.
*
* @return true when `forEach()` has been called on this stream
*/
hasReader(): boolean;
}
/**
* Readable part of a generic Stream, which contains handy helpers such as
* .map() in addition to the basic requirements of a Readable interface.
*/
export interface ReadableStream<T> extends Readable<T>, CommonStream<T> {
/**
* Run all input values through a mapping callback, which must produce a new
* value (or promise for a value), similar to e.g. `Array`'s `map()`.
*
* Stream end in the input stream (normal or with error) will be passed to
* the output stream, after awaiting the result of the optional ender.
*
* Any error (thrown or rejection) in mapper or ender is returned to the
* input stream.
*
* @param mapper Callback which receives each value from this stream, and
* must produce a new value (or promise for a value)
* @param ender Called when stream is ending, result is waited for before
* passing on `end()`
* @param aborter Called when stream is aborted
* @return New stream with mapped values
*/
map<R>(
mapper: (value: T) => R | PromiseLike<R>,
ender?: (error?: Error) => void | PromiseLike<void>,
aborter?: (error: Error) => void
): ReadableStream<R>;
/**
* Run all input values through a filtering callback. If the filter callback
* returns a truthy value (or a promise for a truthy value), the input value
* is written to the output stream, otherwise it is ignored.
* Similar to e.g. `Array`'s `filter()`.
*
* Stream end in the input stream (normal or with error) will be passed to
* the output stream, after awaiting the result of the optional ender.
*
* Any error (thrown or rejection) in mapper or ender is returned to the
* input stream.
*
* @param filterer Callback which receives each value from this stream,
* input value is written to output if callback returns a
* (promise for) a truthy value.
* @param ender Called when stream is ending, result is waited for before
* passing on `end()`
* @param aborter Called when stream is aborted
* @return New stream with filtered values.
*/
filter(
filterer: (value: T) => boolean | PromiseLike<boolean>,
ender?: (error?: Error) => void | PromiseLike<void>,
aborter?: (error: Error) => void
): ReadableStream<T>;
/**
* Reduce the stream into a single value by calling a reducer callback for
* each value in the stream. Similar to `Array#reduce()`.
*
* The output of the previous call to `reducer` (aka `accumulator`) is given
* as the first argument of the next call. For the first call, either the
* `initial` value to `reduce()` is passed, or the first value of the stream
* is used (and `current` will be the second value).
*
* The result of `reduce()` is a promise for the last value returned by
* `reducer` (or the initial value, if there were no calls to `reducer`).
* If no initial value could be determined, the result is rejected with a
* TypeError.
* If the stream is ended with an error, the result is rejected with that
* error.
*
* It is possible for `reducer` to return a promise for its result.
*
* If the `reducer` throws an error or returns a rejected promise, the
* originating `write()` will fail with that error.
*
* Examples:
* s.reduce((acc, val) => acc + val); // sums all values
* s.reduce((acc, val) => { acc.push(val); return acc; }, []); // toArray()
*
* @param reducer Callback called for each value in the stream, with
* accumulator, current value, index of current value, and
* this stream.
* @param initial Optional initial value for accumulator. If no initial
* value is given, first value of stream is used.
* @return Promise for final accumulator.
*/
reduce(
reducer: (
accumulator: T,
current: T,
index: number,
stream: ReadableStream<T>
) => T | PromiseLike<T>,
initial?: T
): Promise<T>;
/**
* Reduce the stream into a single value by calling a reducer callback for
* each value in the stream. Similar to `Array#reduce()`.
*
* The output of the previous call to `reducer` (aka `accumulator`) is given
* as the first argument of the next call. For the first call, either the
* `initial` value to `reduce()` is passed, or the first value of the stream
* is used (and `current` will be the second value).
*
* The result of `reduce()` is a promise for the last value returned by
* `reducer` (or the initial value, if there were no calls to `reducer`).
* If no initial value could be determined, the result is rejected with a
* TypeError.
* If the stream is ended with an error, the result is rejected with that
* error.
*
* It is possible for `reducer` to return a promise for its result.
*
* If the `reducer` throws an error or returns a rejected promise, the
* originating `write()` will fail with that error.
*
* Examples:
* s.reduce((acc, val) => acc + val); // sums all values
* s.reduce((acc, val) => { acc.push(val); return acc; }, []); // toArray()
*
* @param reducer Callback called for each value in the stream, with
* accumulator, current value, index of current value, and
* this stream.
* @param initial Optional initial value for accumulator. If no initial
* value is given, first value of stream is used.
* @return Promise for final accumulator.
*/
reduce<R>(
reducer: (
accumulator: R,
current: T,
index: number,
stream: ReadableStream<T>
) => PromiseLike<R>,
initial: R
): Promise<R>;
/**
* Reduce the stream into a single value by calling a reducer callback for
* each value in the stream. Similar to `Array#reduce()`.
*
* The output of the previous call to `reducer` (aka `accumulator`) is given
* as the first argument of the next call. For the first call, either the
* `initial` value to `reduce()` is passed, or the first value of the stream
* is used (and `current` will be the second value).
*
* The result of `reduce()` is a promise for the last value returned by
* `reducer` (or the initial value, if there were no calls to `reducer`).
* If no initial value could be determined, the result is rejected with a
* TypeError.
* If the stream is ended with an error, the result is rejected with that
* error.
*
* It is possible for `reducer` to return a promise for its result.
*
* If the `reducer` throws an error or returns a rejected promise, the
* originating `write()` will fail with that error.
*
* Examples:
* s.reduce((acc, val) => acc + val); // sums all values
* s.reduce((acc, val) => { acc.push(val); return acc; }, []); // toArray()
*
* @param reducer Callback called for each value in the stream, with
* accumulator, current value, index of current value, and
* this stream.
* @param initial Optional initial value for accumulator. If no initial
* value is given, first value of stream is used.
* @return Promise for final accumulator.
*/
reduce<R>(
// tslint:disable-next-line:unified-signatures
reducer: (
accumulator: R,
current: T,
index: number,
stream: ReadableStream<T>
) => R,
initial: R
): Promise<R>;
/**
* Read all stream values into an array.
*
* Returns a promise that resolves to that array if the stream ends
* normally, or to the error if the stream is ended with an error.
*
* @return Promise for an array of all stream values
*/
toArray(): Promise<T[]>;
/**
* Read all values and end-of-stream from this stream, writing them to
* `writable`.
*
* @param writable Destination stream
* @return The stream passed in, for easy chaining
*/
pipe<R extends Writable<T>>(writable: R): R;
/**
* Return a new stream with the results of running the given
* transform.
*
* @param transformer Function that receives this stream and result stream
* as inputs.
* @return Readable stream with the transformed results
*/
transform<R>(transformer: Transform<T, R>): ReadableStream<R>;
}
/**
* Writable part of a generic Stream, which contains handy helpers such as
* .mappedBy() in addition to the basic requirements of a Writable interface.
*/
export interface WritableStream<T> extends Writable<T>, CommonStream<T> {
/**
* Repeatedly call `writer` and write its returned value (or promise for it)
* to the stream.
* The stream is ended when `writer` returns `undefined` (or a promise that
* resolves to `undefined`).
*
* `writer` is only called again when its previously returned value has been
* processed by the stream.
*
* If writing of a value fails (either by the `writer` throwing an error,
* returning a rejection, or the write call failing), the stream is aborted
* and ended with that error.
*
* `ender` is always called once, just before the stream is ended. I.e.
* after `writer` returned (a promise for) `undefined` or the stream is
* aborted.
* It can be used to e.g. close a resource such as a database.
* Note: `ender` will only be called when `writer`'s promise (if any) has
* resolved.
*
* `aborter` is called once iff the stream is aborted. It can be called
* while e.g. a promise returned from the writer or ender is still pending,
* and can be used to make sure that that promise is resolved/rejected
* sooner.
* Note: the aborter should be considered a 'signal to abort', but cleanup
* of resources should be done in the `ender` (i.e. it cannot return a
* promise).
* It can be called (long) after `ender` has been called, because a stream
* can be aborted even after it is already ended, which is useful if this
* stream element is part of a larger chain of streams.
* An aborter must never throw an error.
*
* @param writer Called when the next value can be written to the stream,
* should return (a promise for) a value to be written,
* or `undefined` (or void promise) to end the stream.
* Will always be called asynchronously.
* @param ender Optional callback called once after `writer` indicated
* end-of-stream, or when the stream is aborted (and
* previously written value resolved/rejected). It's called
* without an argument if stream was not aborted (yet), and
* the abort reason if it was aborted (`aborter` will have
* been called, too). Will always be called asynchronously.
* @param aborter Optional callback called once when stream is aborted.
* Receives abort reason as its argument. Should be used
* to prematurely terminate any pending promises of
* `writer` or `ender`. Will always be called
* asynchronously. Can be called before and after
* `writer` or `ender` have been called, even when `ender`
* is completely finished (useful to e.g. abort other streams, which may
* not be aborted yet).
* Must not throw any errors, will lead to unhandled
* rejected promise if it does.
* @return Promise for completely finished stream, i.e. same promise as `result()`
*/
writeEach(
writer: () => T | undefined | void | PromiseLike<T | undefined | void>,
ender?: (abortReason?: Error) => void | PromiseLike<void>,
aborter?: (abortReason: Error) => void
): Promise<void>;
// TODO Experimental
// TODO Not sure whether a 'reverse' function confuses more than it helps
mappedBy<X>(mapper: (value: X) => T | PromiseLike<T>): WritableStream<X>;
// TODO Experimental
// TODO Not sure whether a 'reverse' function confuses more than it helps
filterBy(
filterer: (value: T) => boolean | PromiseLike<boolean>
): WritableStream<T>;
}
/**
* Used when writing to an already-ended stream.
*/
export class WriteAfterEndError extends BaseError {
constructor() {
super("WriteAfterEndError", "stream already ended");
}
}
/**
* Used when read callback(s) have already been attached.
*/
export class AlreadyHaveReaderError extends BaseError {
constructor() {
super("AlreadyHaveReaderError", "stream already has a reader");
}
}
/**
* Special internal 'error' value to indicate normal stream end.
*/
const EOF = new Error("eof");
/**
* Special end-of-stream value, optionally signalling an error.
*/
class Eof {
public error?: Error;
public result?: PromiseLike<void>;
/**
* Create new end-of-stream value, optionally signalling an error.
* @param error Optional Error value
* @param result Optional final result value of `result()`
*/
constructor(error?: Error, result?: PromiseLike<void>) {
this.error = error;
this.result = result;
}
}
/**
* Written value-promise and a function to resolve the corresponding `write()`
* call's return promise.
*/
interface WriteItem<T> {
/**
* Resolver `write()`'s returned promise
*/
resolveWrite: (done?: void | PromiseLike<void>) => void;
/**
* Promise for value passed to `write()`.
* Either a special Eof value, or a value of type `T`.
*/
value: Eof | TrackedPromise<T>;
}
/**
* Object stream with seamless support for backpressure, ending and error
* handling.
*/
export class Stream<T> implements ReadableStream<T>, WritableStream<T> {
/**
* Writers waiting for `_reader` to retrieve and process their value.
*/
private _writers: WriteItem<T>[] = [];
/**
* Read handler that is called for every written value, as set by
* `forEach()`.
*/
private _reader?: (value: T) => void | PromiseLike<void>;
/**
* End handler that is called when the stream is ended, as set by
* `forEach()`. Note that `forEach()` installs a default handler if the user
* did not supply one.
* Set to 'undefined' when it has been called.
*/
private _ender?: (error?: Error) => void | PromiseLike<void>;
/**
* Abort handler that is called when the stream is aborted, as set by
* `forEach()` (can be undefined).
* Set to 'undefined' when it has been called.
*/
private _aborter?: (error: Error) => void;
/**
* When a written value is being processed by the `_reader`, this property
* is set to a promise that resolves when the reader's returned PromiseLike is
* resolved (or rejected).
*/
private _readBusy?: TrackedPromise<void>;
/**
* Set to an instance of an Eof object, containing optional error and final
* result of this stream. Set when `end()` is called.
*/
private _ending?: Eof;
/**
* Set to an instance of an Eof object, containing optional error and final
* result of this stream. Set when `_ender` is being called but not finished
* yet, unset when `_ended` is set.
*/
private _endPending?: Eof;
/**
* Set to the error passed to `end()` (or the special value `eof`) when the
* stream has ended, and the operation was confirmed by the `_ender`.
*/
private _ended?: Error;
/**
* Set to a rejected promise when the stream is explicitly `abort()`'ed.
*/
private _abortPromise?: Promise<void>;
/**
* Error given in abort() method
*/
private _abortReason?: Error;
/**
* Resolved to a rejection when `abort()` is called.
*/
private _abortDeferred: Deferred<never> = defer<never>();
/**
* Resolved to the result of calling `_ender`, then the `result` property of
* the end-of-stream value.
*/
private _resultDeferred: Deferred<void> = defer();
/**
* Write value (or promise for value) to stream.
*
* Writer is blocked until the value is read by the read handler passed to
* `forEach()`, and the value returned by that read handler is resolved.
*
* It is an error to write an `undefined` value (as this is a common
* programming error). Writing a promise for a void is currently allowed,
* but discouraged.
*
* The promise returned by `write()` will be rejected with the same reason if:
* - the written value is a PromiseLike that resolves to a rejection
* - the read handler throws an error or returns a rejected promise
* It is still possible to write another value after that, or e.g. `end()`
* the stream with or without an error.
*
* @param value Value to write, or promise for it
* @return Void-promise that resolves when value was processed by reader
*/
public write(value: T | PromiseLike<T>): Promise<void> {
if (value === undefined) {
// Technically, we could allow this, but it's a common programming
// error to forget to return a value, and it's arguable whether it's
// useful to have a stream of void's, so let's prevent it for now.
// NOTE: This behaviour may change in the future
// NOTE: writeEach() currently DOES use `undefined` to signal EOF
// TODO: prevent writing a void PromiseLike too?
return Promise.reject(
new TypeError(
"cannot write void value, use end() to end the stream"
)
);
}
const writeDone = defer();
this._writers.push({
resolveWrite: writeDone.resolve,
value: track<T>(Promise.resolve(value)),
});
this._pump();
return writeDone.promise;
}
/**
* End the stream, optionally passing an error.
*
* Already pending writes will be processed by the reader passed to
* `forEach()` before passing the end-of-stream to its end handler.
*
* The returned promise will resolve after the end handler has finished
* processing. It is rejected if the end handler throws an error or returns
* a rejected promise.
*
* All calls to `write()` or `end()` after the first `end()` will be
* rejected with a `WriteAfterEndError`.
*
* By default, this stream's `result()` will be resolved when `end()`
* resolves, or rejected with the error if `end()` is called with an error.
* It is possible to let this stream's `result()` 'wait' until any upstream
* streams have completed by e.g. passing that upstream's `result()` as the
* second argument to `end()`.
*
* Note: even if a stream is aborted, it is still necessary to call `end()`
* to allow any resources to correctly be cleaned up.
*
* @param error Optional Error to pass to `forEach()` end handler
* @param result Optional promise that determines final value of `result()`
* @return Void-promise that resolves when `ended`-handler has processed the
* end-of-stream
*/
public end(error?: Error, endedResult?: PromiseLike<void>): Promise<void> {
if (
!(error === undefined || error === null || error instanceof Error)
) {
// tslint:disable-line:no-null-keyword
return Promise.reject(
new TypeError(
"invalid argument to end(): must be undefined, null or Error object"
)
);
}
const eof = new Eof(error, endedResult);
if (!this._ending && !this._ended) {
this._ending = eof;
}
const writeDone = defer();
const item: WriteItem<T> = {
resolveWrite: writeDone.resolve,
value: eof,
};
this._writers.push(item);
this._pump();
return writeDone.promise;
}
/**
* Read all values from stream, optionally waiting for explicit stream end.
*
* `reader` is called for every written value.
*
* `ender` is called once, when the writer `end()`s the stream, either with
* or without an error.
*
* `reader` and `ender` callbacks can return a promise to indicate when the
* value or end-of-stream condition has been completely processed. This
* ensures that a fast writer can never overload a slow reader, and is
* called 'backpressure'.
*
* The `reader` callback is never called while its previously returned
* promise is still pending, and the `ender` callback is likewise only
* called after all reads have completed.
*
* The corresponding `write()` or `end()` operation is blocked until the
* value returned from the reader or ender callback is resolved. If the
* callback throws an error or the returned promise resolves to a rejection,
* the `write()` or `end()` will be rejected with it.
*
* All callbacks are always called asynchronously (i.e. some time after
* `forEach()`, `write()`, `end()` or `abort()` returns), and their `this`
* argument will be undefined.
*
* `aborter` is called once if the stream is aborted and has not ended yet.
* (I.e. it will be called if e.g. `ender`'s returned promise is still
* pending, to allow early termination, but it will no longer be called
* if its promise has resolved).
*
* The `aborter` callback can be called while a reader callback's promise is
* still pending, and should try to let `reader` or `ender` finish as fast
* as possible.
*
* Note that even when a stream is aborted, it still needs to be `end()`'ed
* correctly.
*
* If no `ender` is given, a default end handler is installed that directly
* acknowledges the end-of-stream, also in case of an error. Note that that
* error will still be returned from `forEach()`.
*
* If no `aborter` is given, an abort is ignored (but will still cause
* further writes to fail, and it will be reflected in the returned promise).
*
* It is an error to call `forEach()` multiple times, and it is not possible
* to 'detach' already attached callbacks. Reason is that the exact behaviour
* of such actions (e.g. block or simply ignore further writes) is application
* dependent, and should be implemented as a transform instead.
*
* The return value of `forEach()` is `result()`, a promise that resolves
* when all parts in the stream(-chain) have completely finished.
*
* @param reader Callback called with every written value
* @param ender Optional callback called when stream is ended
* @param aborter Optional callback called when stream is aborted
* @return Promise for completely finished stream, i.e. same promise as `result()`
*/
public forEach(
reader: (value: T) => void | PromiseLike<void>,
ender?: (error?: Error) => void | PromiseLike<void>,
aborter?: (error: Error) => void
): Promise<void> {
if (this.hasReader()) {
return Promise.reject(new AlreadyHaveReaderError());
}
if (!ender) {
// Default ender swallows errors, because they
// will already be signalled in the stream's
// `.result()` (and thus the output of `.forEach()`).
// See #35.
ender = noop;
}
this._reader = reader;
this._ender = ender;
this._aborter = aborter;
this._pump();
return this.result();
}
/**
* Signal that the stream is aborted: it will no longer read incoming
* elements and will no longer write elements except in the course of
* processing any pending asynchronous reader callbacks (i.e. unresolved
* Promises returned by `forEach()` or other stream iterators). Does not
* end the stream.
*
* An upstream source can handle this `abort()` by catching the exception
* from its own `aborted()` method--for example, to cancel pending fetch
* operations, or close a continuous data stream.
*
* If the stream's `forEach()` function provided an `aborter` callback and
* the stream is not yet ended, `aborter` will be called with the abort reason.
* This can be used to cancel any remaining operations inside the asynchronous
* reader callback.
*
* Once the last pending callback is resolved, any pending and future `write()`s
* to this stream will be rejected with the error provided to `abort()`.
*
* It is still necessary to explicitly `end()` the stream, to ensure that any
* resources can be cleaned up correctly both on the reader and writer side.
* The stream's `ender` callback will be called with the abort error (i.e. any
* error passed to `end()` is ignored.)
*
* The abort is ignored if the stream is already aborted.
*
* It's possible to abort an ended stream. This can be used to 'bubble' an
* abort signal to other parts in a chain of streams which may not have ended
* yet. It will not change the end-state of this part of the stream though.
*
* @param reason Optional Error value to signal a reason for the abort
*/
public abort(reason?: Error): void {
if (this._abortPromise) {
return;
}
if (!reason) {
reason = new Error("aborted");
}
this._abortDeferred.reject(reason);
this._abortPromise = this._abortDeferred.promise;
this._abortReason = reason;
this._pump();
}
/**
* Obtain promise that resolves to a rejection when `abort()` is called.
*
* Useful to pass abort to up- and down-stream sources.
*
* Note: this promise either stays pending, or is rejected. It is never
* fulfilled.
*
* @return Promise that is rejected with abort error when stream is aborted
*/
public aborted(): Promise<never> {
return this._abortDeferred.promise;
}
/**
* Obtain a promise that resolves when the stream has completely ended:
* - `end()` has been called (possibly with an Error),
* - `ender` callback has run and its returned promise resolved,
* - `end()`'s result parameter (if any) has been resolved.
*
* @return Promise resolved when stream has completely ended
*/
public result(): Promise<void> {
return this._resultDeferred.promise;
}
/**
* Determine whether `end()` has been called on the stream, but the stream
* is still processing it.
*
* @return true when `end()` was called but not acknowledged yet, false
* otherwise
*/
public isEnding(): boolean {
return !!this._ending;
}
/**
* Determine whether stream has completely ended (i.e. end handler has been
* called and its return PromiseLike, if any, is resolved).
* @return true when stream has ended, false otherwise
*/
public isEnded(): boolean {
return !!this._ended;
}
/**
* Determine whether `end()` has been called on the stream.
*
* @return true when `end()` was called
*/
public isEndingOrEnded(): boolean {
return this.isEnding() || this.isEnded();
}
/**
* Determine whether `forEach()` callback(s) are currently attached to the
* stream.
*
* @return true when `forEach()` has been called on this stream
*/
public hasReader(): boolean {
return !!this._reader;
}
/**
* Run all input values through a mapping callback, which must produce a new
* value (or promise for a value), similar to e.g. `Array`'s `map()`.
*
* Stream end in the input stream (normal or with error) will be passed to
* the output stream, after awaiting the result of the optional ender.
*
* Any error (thrown or rejection) in mapper or ender is returned to the
* input stream.
*
* @param mapper Callback which receives each value from this stream, and
* must produce a new value (or promise for a value)
* @param ender Called when stream is ending, result is waited for before
* passing on `end()`
* @param aborter Called when stream is aborted
* @return New stream with mapped values
*/
public map<R>(
mapper: (value: T) => R | PromiseLike<R>,
ender?: (error?: Error) => void | PromiseLike<void>,
aborter?: (error: Error) => void
): ReadableStream<R> {
const output = new Stream<R>();
map(this, output, mapper, ender, aborter);
return output;
}
/**
* Run all input values through a filtering callback. If the filter callback
* returns a truthy value (or a promise for a truthy value), the input value
* is written to the output stream, otherwise it is ignored.
* Similar to e.g. `Array`'s `filter()`.
*
* Stream end in the input stream (normal or with error) will be passed to
* the output stream, after awaiting the result of the optional ender.
*
* Any error (thrown or rejection) in mapper or ender is returned to the
* input stream.
*
* @param filterer Callback which receives each value from this stream,
* input value is written to output if callback returns a
* (promise for) a truthy value.
* @param ender Called when stream is ending, result is waited for before
* passing on `end()`
* @param aborter Called when stream is aborted
* @return New stream with filtered values.
*/
public filter(
filterer: (value: T) => boolean | PromiseLike<boolean>,
ender?: (error?: Error) => void | PromiseLike<void>,
aborter?: (error: Error) => void
): ReadableStream<T> {
const output = new Stream<T>();
filter(this, output, filterer, ender, aborter);
return output;
}
/**
* Reduce the stream into a single value by calling a reducer callback for
* each value in the stream. Similar to `Array#reduce()`.
*
* The output of the previous call to `reducer` (aka `accumulator`) is given
* as the first argument of the next call. For the first call, either the
* `initial` value to `reduce()` is passed, or the first value of the stream
* is used (and `current` will be the second value).
*
* The result of `reduce()` is a promise for the last value returned by
* `reducer` (or the initial value, if there were no calls to `reducer`).
* If no initial value could be determined, the result is rejected with a
* TypeError.
* If the stream is ended with an error, the result is rejected with that
* error.
*
* It is possible for `reducer` to return a promise for its result.
*
* If the `reducer` throws an error or returns a rejected promise, the
* originating `write()` will fail with that error.
*
* Examples:
* s.reduce((acc, val) => acc + val); // sums all values
* s.reduce((acc, val) => { acc.push(val); return acc; }, []); // toArray()
*
* @param reducer Callback called for each value in the stream, with
* accumulator, current value, index of current value, and
* this stream.
* @param initial Optional initial value for accumulator. If no initial
* value is given, first value of stream is used.
* @return Promise for final accumulator.
*/
public reduce(
reducer: (
accumulator: T,
current: T,
index: number,
stream: ReadableStream<T>
) => T | PromiseLike<T>,
initial?: T
): Promise<T>;
/**
* Reduce the stream into a single value by calling a reducer callback for
* each value in the stream. Similar to `Array#reduce()`.
*
* The output of the previous call to `reducer` (aka `accumulator`) is given
* as the first argument of the next call. For the first call, either the
* `initial` value to `reduce()` is passed, or the first value of the stream
* is used (and `current` will be the second value).
*
* The result of `reduce()` is a promise for the last value returned by
* `reducer` (or the initial value, if there were no calls to `reducer`).
* If no initial value could be determined, the result is rejected with a
* TypeError.
* If the stream is ended with an error, the result is rejected with that
* error.
*
* It is possible for `reducer` to return a promise for its result.
*
* If the `reducer` throws an error or returns a rejected promise, the
* originating `write()` will fail with that error.
*
* Examples:
* s.reduce((acc, val) => acc + val); // sums all values
* s.reduce((acc, val) => { acc.push(val); return acc; }, []); // toArray()
*
* @param reducer Callback called for each value in the stream, with
* accumulator, current value, index of current value, and
* this stream.
* @param initial Optional initial value for accumulator. If no initial
* value is given, first value of stream is used.
* @return Promise for final accumulator.
*/
public reduce<R>(
reducer: (
accumulator: R,
current: T,
index: number,
stream: ReadableStream<T>
) => PromiseLike<R>,
initial: R
): Promise<R>;
/**
* Reduce the stream into a single value by calling a reducer callback for
* each value in the stream. Similar to `Array#reduce()`.
*
* The output of the previous call to `reducer` (aka `accumulator`) is given
* as the first argument of the next call. For the first call, either the
* `initial` value to `reduce()` is passed, or the first value of the stream
* is used (and `current` will be the second value).
*
* The result of `reduce()` is a promise for the last value returned by
* `reducer` (or the initial value, if there were no calls to `reducer`).
* If no initial value could be determined, the result is rejected with a
* TypeError.
* If the stream is ended with an error, the result is rejected with that
* error.
*
* It is possible for `reducer` to return a promise for its result.
*
* If the `reducer` throws an error or returns a rejected promise, the
* originating `write()` will fail with that error.
*
* Examples:
* s.reduce((acc, val) => acc + val); // sums all values
* s.reduce((acc, val) => { acc.push(val); return acc; }, []); // toArray()
*
* @param reducer Callback called for each value in the stream, with
* accumulator, current value, index of current value, and
* this stream.
* @param initial Optional initial value for accumulator. If no initial
* value is given, first value of stream is used.
* @return Promise for final accumulator.
*/
public reduce<R>(
// tslint:disable-next-line:unified-signatures
reducer: (
accumulator: R,
current: T,
index: number,
stream: ReadableStream<T>
) => R,
initial: R
): Promise<R> {
let haveAccumulator = arguments.length === 2;
let accumulator: any = initial;
let index = 0;
return this.forEach((value: T): void | PromiseLike<void> => {
if (!haveAccumulator) {
accumulator = value;
haveAccumulator = true;
index++;
} else {
return Promise.resolve(
reducer(accumulator, value, index++, this)
).then((newAccumulator: any) => (accumulator = newAccumulator));
}
}).then(() => {
if (!haveAccumulator) {
return Promise.reject<R>(
new TypeError(
"cannot reduce() empty stream without initial value"
)
);
}
return accumulator;
});
}
/**
* Read all stream values into an array.
*
* Returns a promise that resolves to that array if the stream ends
* normally, or to the error if the stream is ended with an error.
*
* @return Promise for an array of all stream values
*/
public toArray(): Promise<T[]> {
const result: T[] = [];
return this.forEach((value: T) => {
result.push(value);
}).then(() => result);
}
/**
* Read all values and end-of-stream from this stream, writing them to
* `writable`.
*
* @param writable Destination stream
* @return The stream passed in, for easy chaining
*/
public pipe<R extends Writable<T>>(writable: R): R {
writable.aborted().catch((err) => this.abort(err));
this.aborted().catch((err) => writable.abort(err));
this.forEach(
(value: T) => writable.write(value),
(error?: Error) => writable.end(error, this.result())
);
return writable;
}
/**
* Return a new stream with the results of running the given
* transform.
*
* @param transformer Function that receives this stream and result stream
* as inputs.
* @return Readable stream with the transformed results
*/
public transform<R>(transformer: Transform<T, R>): ReadableStream<R> {
const output = new Stream<R>();
transformer(this, output);
return output;
}
/**
* Repeatedly call `writer` and write its returned value (or promise for it)
* to the stream.
* The stream is ended when `writer` returns `undefined` (or a promise that
* resolves to `undefined`).
*
* `writer` is only called again when its previously returned value has been
* processed by the stream.
*
* If writing of a value fails (either by the `writer` throwing an error,
* returning a rejection, or the write call failing), the stream is aborted
* and ended with that error.
*
* `ender` is always called once, just before the stream is ended. I.e.
* after `writer` returned (a promise for) `undefined` or the stream is
* aborted.
* It can be used to e.g. close a resource such as a database.
* Note: `ender` will only be called when `writer`'s promise (if any) has
* resolved.
*
* `aborter` is called once iff the stream is aborted. It can be called
* while e.g. a promise returned from the writer or ender is still pending,
* and can be used to make sure that that promise is resolved/rejected
* sooner.
* Note: the aborter should be considered a 'signal to abort', but cleanup
* of resources should be done in the `ender` (i.e. it cannot return a
* promise).
* It can be called (long) after `ender` has been called, because a stream
* can be aborted even after it is already ended, which is useful if this
* stream element is part of a larger chain of streams.
* An aborter must never throw an error.
*
* @param writer Called when the next value can be written to the stream,
* should return (a promise for) a value to be written,
* or `undefined` (or void promise) to end the stream.
* Will always be called asynchronously.
* @param ender Optional callback called once after `writer` indicated
* end-of-stream, or when the stream is aborted (and
* previously written value resolved/rejected). It's called
* without an argument if stream was not aborted (yet), and
* the abort reason if it was aborted (`aborter` will have
* been called, too). Will always be called asynchronously.
* @param aborter Optional callback called once when stream is aborted.
* Receives abort reason as its argument. Should be used
* to prematurely terminate any pending promises of
* `writer` or `ender`. Will always be called
* asynchronously. Can be called before and after
* `writer` or `ender` have been called, even when `ender`
* is completely finished (useful to e.g. abort other streams, which may
* not be aborted yet).
* Must not throw any errors, will lead to unhandled
* rejected promise if it does.
* @return Promise for completely finished stream, i.e. same promise as `result()`
*/
public writeEach(
writer: () => T | undefined | void | PromiseLike<T | undefined | void>,
ender?: (abortReason?: Error) => void | PromiseLike<void>,
aborter?: (abortReason: Error) => void
): Promise<void> {
/**
* Call aborter (if any) and convert any thrown error into
* unhandled rejection. Unset aborter to prevent calling it
* again later.
*/
const callAborter = (abortReason: Error) => {
if (!aborter) {
return;
}
try {
const callback = aborter;
aborter = undefined;
callback(abortReason);
} catch (aborterError) {
// Convert into unhandled rejection. There's not really
// a sensible way to convert it into something else.
// One might think to pass it to the ender, which may
// be undefined, or this.end(), but that seams rather
// unexpected to occassionally have to handle an error
// from one specific aborter, whereas other aborters's
// errors will also lead to unhandled rejections.
// Note: the most sensible thing to do now, is to
// terminate the program.
Promise.reject(aborterError);
}
};
const worker = async () => {
try {
while (!this._abortPromise) {
const value = await writer();
if (value === undefined) {
break;
} else {
await this.write(value as T);
}
}
} catch (writeError) {
this.abort(writeError);
} finally {
// If our writer caused the abort, make sure to
// call ender (and aborter) with that reason. Other aborts
// may happen at any time, so they may be caught by this,
// or they may be caught by the out-of-loop asynchronous
// callback registered below.
let endError = this._abortReason; // Will be `undefined` in normal cases
if (this._abortReason) {
callAborter(this._abortReason);
}
if (ender) {
try {
await (this._abortReason
? ender(this._abortReason)
: ender());
} catch (error) {
endError = error;
}
}
await this.end(endError);
}
};
// Asynchronously call worker, abort on error
Promise.resolve()
.then(worker)
.catch((error: Error) => this.abort(error));
// Ensure aborter is asynchronously called if necessary
this.aborted().catch(callAborter);
return this.result();
}
// TODO Experimental
// TODO Not sure whether a 'reverse' function confuses more than it helps
public mappedBy<X>(
mapper: (value: X) => T | PromiseLike<T>
): Writ