|
|
@@ -1,7 +1,193 @@
|
|
|
+import * as stream from "stream";
|
|
|
+import * as NodeUtils from "util";
|
|
|
|
|
|
-interface Source<T> {
|
|
|
- map<V>(mapper: (t: T) => V): Source<V>
|
|
|
- runWith<M>(sink: Sink<T, M>): Promise<T>
|
|
|
+export const Done = Symbol("Done");
|
|
|
+export type Done = typeof Done;
|
|
|
+
|
|
|
+interface Sink<T, Mat> {
|
|
|
+ builder: SinkBuilder<Mat>;
|
|
|
+ __phanthom__: T;
|
|
|
+}
|
|
|
+
|
|
|
+type RawSource<T> = IterableSource<T> | MappedSource<any, T>;
|
|
|
+
|
|
|
+interface MappedSource<T, V> {
|
|
|
+ _type: "MappedSource";
|
|
|
+ source: RawSource<T>;
|
|
|
+ mapFn: (t: T) => V;
|
|
|
+}
|
|
|
+
|
|
|
+interface IterableSource<T> {
|
|
|
+ _type: "IterableSource";
|
|
|
+ it: Iterable<T>;
|
|
|
+}
|
|
|
+
|
|
|
+type ClosedPipeline<T, M> = {
|
|
|
+ source: Source<T>;
|
|
|
+ sink: Sink<T, M>;
|
|
|
+};
|
|
|
+
|
|
|
+interface SinkBuilder<M> {
|
|
|
+ buildWritable(out: (value: M) => void): stream.Writable;
|
|
|
+}
|
|
|
+
|
|
|
+export namespace Sink {
|
|
|
+ export const sum: Sink<number, number> = {
|
|
|
+ builder: {
|
|
|
+ buildWritable(out) {
|
|
|
+ let result = 0;
|
|
|
+ return new stream.Writable({
|
|
|
+ objectMode: true,
|
|
|
+ write(v, _, onNext) {
|
|
|
+ result += v;
|
|
|
+ onNext();
|
|
|
+ },
|
|
|
+ }).on("finish", () => out(result));
|
|
|
+ },
|
|
|
+ },
|
|
|
+ __phanthom__: Phantom(),
|
|
|
+ };
|
|
|
+
|
|
|
+ export const ignore: Sink<any, Done> = {
|
|
|
+ builder: {
|
|
|
+ buildWritable(out) {
|
|
|
+ return new stream.Writable({
|
|
|
+ objectMode: true,
|
|
|
+ write(v, _, onNext) {
|
|
|
+ onNext();
|
|
|
+ },
|
|
|
+ }).on("finish", () => out(Done));
|
|
|
+ },
|
|
|
+ },
|
|
|
+ __phanthom__: Phantom(),
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+export class Source<T> {
|
|
|
+ private __phanthom__!: T;
|
|
|
+
|
|
|
+ private constructor(private builder: SourceBuilder<T>) {}
|
|
|
+
|
|
|
+ map<O>(f: (v: T) => O): Source<O> {
|
|
|
+ return new Source(MappedSourceBuilder(this.builder, f));
|
|
|
+ }
|
|
|
+
|
|
|
+ grouped(n: number): Source<T[]> {
|
|
|
+ return new Source(
|
|
|
+ BatchedSourceBuilder(this.builder, n)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ into<M>(sink: Sink<T, M>) {
|
|
|
+ const self = this;
|
|
|
+ return {
|
|
|
+ run() {
|
|
|
+ let result: undefined | M;
|
|
|
+ const stages = [
|
|
|
+ ...self.builder.build(),
|
|
|
+ sink.builder.buildWritable((v) => {
|
|
|
+ result = v;
|
|
|
+ }),
|
|
|
+ ];
|
|
|
+ return NodeUtils.promisify(stream.pipeline)(stages).then(() => {
|
|
|
+ if (result !== undefined) return result;
|
|
|
+ else throw new Error("output function was not called "); // FIXME find a more error prone API ?
|
|
|
+ });
|
|
|
+ },
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ static fromArray<T>(a: T[]) {
|
|
|
+ return new Source<T>(IterableSourceBuilder(a));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-interface Sink<T, M> {}
|
|
|
+interface SourceBuilder<T> {
|
|
|
+ build(): Array<NodeJS.ReadableStream | NodeJS.ReadWriteStream>;
|
|
|
+}
|
|
|
+
|
|
|
+function MappedSource<T, U>(
|
|
|
+ source: RawSource<T>,
|
|
|
+ mapFn: (t: T) => U
|
|
|
+): MappedSource<T, U> {
|
|
|
+ return { _type: "MappedSource", source, mapFn };
|
|
|
+}
|
|
|
+
|
|
|
+function IterableSource<T>(it: Iterable<T>): IterableSource<T> {
|
|
|
+ return { _type: "IterableSource", it };
|
|
|
+}
|
|
|
+
|
|
|
+function IterableSourceBuilder<T>(it: Iterable<T>) {
|
|
|
+ return {
|
|
|
+ build() {
|
|
|
+ return [stream.Readable.from(it)];
|
|
|
+ },
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function MappedSourceBuilder<T, U>(prev: SourceBuilder<T>, mapFn: (t: T) => U) {
|
|
|
+ return {
|
|
|
+ build() {
|
|
|
+ return [
|
|
|
+ ...prev.build(),
|
|
|
+ new stream.Transform({
|
|
|
+ objectMode: true,
|
|
|
+ transform(v, {}, callback) {
|
|
|
+ callback(undefined, mapFn(v));
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ ];
|
|
|
+ },
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function BatchedSourceBuilder<T>(prev: SourceBuilder<T>, batchSize: number) {
|
|
|
+ let currentBatch: T[] = [];
|
|
|
+ return {
|
|
|
+ build() {
|
|
|
+ return [
|
|
|
+ ...prev.build(),
|
|
|
+ new stream.Transform({
|
|
|
+ objectMode: true,
|
|
|
+ transform(v, {}, callback) {
|
|
|
+ currentBatch.push(v);
|
|
|
+ if (currentBatch.length == batchSize) {
|
|
|
+ callback(undefined, currentBatch);
|
|
|
+ currentBatch = [];
|
|
|
+ } else {
|
|
|
+ callback();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ flush(callback) {
|
|
|
+ callback(undefined, currentBatch)
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ ];
|
|
|
+ },
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function buildStreams<T>(
|
|
|
+ r: RawSource<T>
|
|
|
+): Array<
|
|
|
+ NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream
|
|
|
+> {
|
|
|
+ switch (r._type) {
|
|
|
+ case "IterableSource":
|
|
|
+ return [stream.Readable.from(r.it)];
|
|
|
+ case "MappedSource":
|
|
|
+ return [
|
|
|
+ ...buildStreams(r.source),
|
|
|
+ new stream.Transform({
|
|
|
+ objectMode: true,
|
|
|
+ transform(v, {}, callback) {
|
|
|
+ callback(undefined, r.mapFn(v));
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Phantom<T>(): T {
|
|
|
+ return undefined!;
|
|
|
+}
|