UNPKG

@johngw/stream

Version:

Reactive programming tools using the WHATWG Streams API.

115 lines (105 loc) 3.23 kB
import { Forkable } from '@johngw/stream/sinks/Forkable' import { Controllable } from '@johngw/stream/sources/Controllable' import { ControllableSource } from '@johngw/stream/sources/ControllableSource' import { SourceComposite } from '@johngw/stream/sources/SourceComposite' /** * A ForkableSink is the underlying logic for "1 Writable to many Readables". * * @group Sinks * @see {@link ForkableStream:class} * @example * ``` * const forkable = new ForkableStream<T>() * const writable = new WritableStream(forkable) * * fromCollection([1, 2, 3, 4, 5, 6, 7]).pipeTo(writable) * * forkable.fork().pipeTo(write(x => console.info('fork1', x))) * // fork1 1, fork1 2, fork1 3, fork1 4, fork1 5, fork1 6, fork1 7 * * forkable.fork().pipeTo(write(x => console.info('fork2', x))) * // fork2 1, fork2 2, fork2 3, fork2 4, fork2 5, fork2 6, fork2 7 * ``` */ export class ForkableSink<T> implements UnderlyingSink<T>, Forkable<T> { // eslint-disable-next-line @typescript-eslint/no-explicit-any #error?: any #finished = false #controllers = new WeakMap<Controllable<T>, ReadableStream<T>>() #controllerSet = new Set<Controllable<T>>() abort(reason: unknown) { this.#forEachController((controller) => controller.error(reason)) this.#controllerSet.clear() this.#controllers = new WeakMap() this.#error = reason this.#finished = true } close() { this.#forEachController((controller) => { try { controller.close() } catch (error) { // potentially already closed } }) this.#controllerSet.clear() this.#controllers = new WeakMap() this.#finished = true } write(chunk: T) { this.#forEachController( (controller) => controller.desiredSize && controller.enqueue(chunk) ) } get finished() { return this.#finished } fork( underlyingSource?: UnderlyingDefaultSource<T>, queuingStrategy?: QueuingStrategy<T> ) { return this._pipeThroughController( this._addController(underlyingSource, queuingStrategy) ) } #forEachController( fn: (controller: Controllable<T>, stream: ReadableStream<T>) => void ) { for (const controller of this.#controllerSet) { const stream = this.#controllers.get(controller) if (stream) fn(controller, stream) } } protected _addController( underlyingSource?: UnderlyingDefaultSource<T>, queuingStrategy?: QueuingStrategy<T> ) { const controller = new ControllableSource<T>() const stream = new ReadableStream<T>( new SourceComposite<T>([ controller, underlyingSource || {}, { cancel: () => { this.#controllers.delete(controller) this.#controllerSet.delete(controller) }, }, ]), queuingStrategy ) if (!this.#finished) { this.#controllerSet.add(controller) this.#controllers.set(controller, stream) } return [controller, stream] as const } protected _pipeThroughController([controller, stream]: readonly [ Controllable<T>, ReadableStream<T> ]) { if (this.#error) controller.error(this.#error) else if (this.#finished) controller.close() return stream } }