UNPKG

@rimbu/stream

Version:

Efficient structure representing a sequence of elements, with powerful operations for TypeScript

1,513 lines (1,374 loc) 60.5 kB
import { RimbuError } from '@rimbu/base'; import { AsyncOptLazy, CollectFun, Eq, ErrBase, type AsyncCollectFun, type MaybePromise, } from '@rimbu/common'; import { Reducer, Stream, type AsyncFastIterator, type AsyncStreamSource, } from '@rimbu/stream'; import { AsyncStreamConstructorsImpl, fromAsyncStreamSource, } from '../async-custom/async-stream-custom.mjs'; /** * An `AsyncReducer` is a stand-alone asynchronous calculation that takes input values of type I, * and, when requested, produces an output value of type O. * @typeparam I - the input value type * @typeparam O - the output value type */ export type AsyncReducer<I, O = I> = AsyncReducer.Impl<I, O, unknown>; function identity<T>(value: T): T { return value; } /** * Combines multiple (asynchronous) reducers in an array of the same input type into a single reducer that * forwards each incoming value to all reducers, and when output is requested will return an array containing * the corresponding output of each reducer. */ function combineArr<T, R extends readonly [unknown, unknown, ...unknown[]]>( ...reducers: { [K in keyof R]: AsyncReducer.Accept<T, R[K]> } & AsyncReducer< T, unknown >[] ): AsyncReducer<T, R> { return AsyncReducer.create<T, any, AsyncReducer.Instance<T, unknown>[]>( async (initHalt) => { let allHalted = true; const result = await Promise.all( reducers.map(async (reducer) => { const instance = await AsyncReducer.from(reducer).compile(); allHalted = allHalted && instance.halted; return instance; }) ); if (allHalted) { initHalt(); } return result; }, async (state, elem, index, halt) => { let allHalted = true; await Promise.all( Stream.from(state).mapPure(async (reducer) => { if (reducer.halted) return; await reducer.next(elem); allHalted = allHalted && reducer.halted; }) ); if (allHalted) { halt(); } return state; }, (state) => Promise.all( Stream.from(state).mapPure((reducerInstance) => reducerInstance.getOutput() ) ), async (state, err) => { await Promise.all( Stream.from(state).mapPure((reducer) => reducer.onClose(err)) ); } ); } /** * Combines multiple (asynchronous) reducers in an object's values of the same input type into a single reducer that * forwards each incoming value to all reducers, and when output is requested will return an object containing * the corresponding output of each reducer at the matching object property. */ function combineObj<T, R extends { readonly [key: string]: unknown }>( reducerObj: { readonly [K in keyof R]: AsyncReducer.Accept<T, R[K]>; } & Record<string, AsyncReducer.Accept<T, unknown>> ): AsyncReducer<T, R> { return AsyncReducer.create( async (initHalt) => { const result: Record<string, AsyncReducer.Instance<T, any>> = {}; let allHalted = true; await Promise.all( Stream.fromObject( reducerObj as Record<string, AsyncReducer.Accept<T, unknown>> ).mapPure(async ([key, reducer]) => { const instance = await AsyncReducer.from(reducer).compile(); result[key] = instance; allHalted = allHalted && instance.halted; }) ); if (allHalted) { initHalt(); } return result; }, async (state, elem, index, halt) => { let allHalted = true; await Promise.all( Stream.fromObjectValues(state).mapPure(async (reducerInstance) => { if (!reducerInstance.halted) { await reducerInstance.next(elem); allHalted = allHalted && reducerInstance.halted; } }) ); if (allHalted) { halt(); } return state; }, async (state) => { const result: any = {}; await Promise.all( Stream.fromObject(state).mapPure(async ([key, reducerInstance]) => { result[key] = await reducerInstance.getOutput(); }) ); return result; }, async (state, err) => { await Promise.all( Stream.fromObjectValues(state).mapPure((reducerInstance) => reducerInstance.onClose(err) ) ); } ); } export namespace AsyncReducer { /** * Convenience type to allow synchronous reducers to be supplied to functions that accept async reducers. * @typeparam I - the input type * @typeparam O - the output type */ export type Accept<I, O> = AsyncReducer<I, O> | Reducer<I, O>; /** * The AsyncReducer implementation interface defining the required methods. * @typeparam I - the input type * @typeparam O - the output type * @typeparam S - the state type */ export interface Impl<I, O, S> { /** * The initial state value for the reducer algorithm. * @param initHalt - a callback function that, if called, indicates that the reducer does not accept any input. */ readonly init: (initHalt: () => void) => MaybePromise<S>; /** * Returns the next state based on the given input values * @param state - the current state * @param elem - the current input value * @param index - the current input index * @param halt - a function that, when called, ensures no more values are passed to the reducer */ next(state: S, elem: I, index: number, halt: () => void): MaybePromise<S>; /** * Returns the output value based on the given `state` * @param state - the current state * @param index - the value index * @param halted - a boolean indicating whether the reducer is halted */ stateToResult(state: S, index: number, halted: boolean): MaybePromise<O>; /** * An optional function that is called when the reducer will no longer receive values. * @param state - the final reducer state * @param error - (optional) if an error has occured, it ix passed here */ onClose?: ((state: S, error?: unknown) => MaybePromise<void>) | undefined; /** * Returns an `AsyncReducer` instance that only passes values to the reducer that satisy the given `pred` predicate. * @param pred - a potaentially asynchronous function that returns true if the value should be passed to the reducer based on the following inputs:<br/> * - value: the current input value<br/> * - index: the current input index<br/> * - halt: function that, when called, ensures no more new values are passed to the reducer * @param options - (optional) an object containing the following properties:<br/> * - negate: (default: false) when true will invert the given predicate * @note if the predicate is a type guard, the return type is automatically inferred * @example * ```ts * AsyncReducer * .createMono(0, async (c, v) => c + v) * .filterInput(async v => v > 10) * // this reducer will only sum values larger than 10 * ``` */ filterInput<IF extends I>( pred: (value: I, index: number, halt: () => void) => value is IF, options?: { negate?: false | undefined } ): AsyncReducer<IF, O>; filterInput<IF extends I>( pred: (value: I, index: number, halt: () => void) => value is IF, options: { negate: true } ): AsyncReducer<Exclude<I, IF>, O>; filterInput( pred: ( value: I, index: number, halt: () => void ) => MaybePromise<boolean>, options?: { negate?: boolean | undefined } ): AsyncReducer<I, O>; /** * Returns an `AsyncReducer` instance that converts its input values using given `mapFun` before passing them to the reducer. * @param mapFun - a potentially asynchronous function that returns a new value to pass to the reducer based on the following inputs:<br/> * - value: the current input value<br/> * - index: the current input index * @typeparam I2 - the new input type * @example * ```ts * AsyncReducer * .createMono(0, async (c, v) => c + v) * .mapInput(async v => v * 2) * // this reducer will double all input values before summing them * ``` */ mapInput: <I2>( mapFun: (value: I2, index: number) => MaybePromise<I> ) => AsyncReducer<I2, O>; /** * Returns an `AsyncReducer` instance that converts its input values using given `flatMapFun` before passing them to the reducer. * @param flatMapFun - a potentially asynchronous function that returns am arbitrary number of new values to pass to the reducer based on the following inputs:<br/> * - value: the current input value<br/> * - index: the current input index * @typeparam I2 - the new input type * @example * ```ts * AsyncReducer * .createMono(0, async (c, v) => c + v) * .flatMapInput(async v => [v, v]) * // this reducer will include all input values twice before summing them * ``` */ flatMapInput<I2>( flatMapFun: ( value: I2, index: number ) => MaybePromise<AsyncStreamSource<I>> ): AsyncReducer<I2, O>; /** * Returns an `AsyncReducer` instance that converts or filters its input values using given `collectFun` before passing them to the reducer. * @typeparam I2 - the new input type * @param collectFun - a (potentially async) function receiving<br/> * - `value`: the next value<br/> * - `index`: the value index<br/> * - `skip`: a token that, when returned, will not add a value to the resulting collection<br/> * - `halt`: a function that, when called, ensures no next elements are passed * @example * ```ts * AsyncReducer * .createMono(0, async (c, v) => c + v) * .collectInput(async (v, _, skip) => v <= 10 ? skip : v * 2) * // this reducer will double all input values larger thant 10 before summing them, * // and will skip all values smaller than 10 * ``` */ collectInput<I2>(collectFun: AsyncCollectFun<I2, I>): AsyncReducer<I2, O>; /** * Returns an `AsyncReducer` instance that converts its output values using given `mapFun`. * @param mapFun - a potentially asynchronous function that takes the current output value and converts it to a new output value * @typeparam O2 - the new output type * @example * ```ts * AsyncReducer * .createMono(0, async (c, v) => c + v) * .mapOutput(async v => String(v)) * // this reducer will convert all its results to string before returning them * ``` */ mapOutput<O2>( mapFun: (value: O, index: number, halted: boolean) => MaybePromise<O2> ): AsyncReducer<I, O2>; /** * Returns an `AsyncReducer` instance that takes at most the given `amount` of input elements, and will ignore subsequent elements. * @param amount - the amount of elements to accept * @example * ```ts * await AsyncStream * .from(Stream.range({ end: 10 })) * .reduce( * AsyncReducer * .createMono(0, async (c, v) => c + v) * .takeInput(2) * ) * // => 1 * ``` */ takeInput(amount: number): AsyncReducer<I, O>; /** * Returns an `AsyncReducer` instance that skips the first given `amount` of input elements, and will process subsequent elements. * @param amount - the amount of elements to skip * @example * ```ts * await AsyncStream * .from(Stream.range({ end: 10 })) * .reduce( * AsyncReducer * .createMono(0, async (c, v) => c + v) * .dropInput(9) * ) * // => 19 * ``` */ dropInput(amount: number): AsyncReducer<I, O>; /** * Returns an `AsyncReducer` instance that takes given `amount` of elements starting at given `from` index, and ignores other elements. * @param from - (default: 0) the index at which to start processing elements * @param amount - (optional) the amount of elements to process, if not given, processes all elements from the `from` index * @example * ```ts * await AsyncStream * .from(Stream.range({ end: 10 })) * .reduce( * AsyncReducer * .createMono(0, async (c, v) => c + v) * .sliceInput(1, 2) * ) * // => 3 * ``` */ sliceInput( from?: number | undefined, amount?: number | undefined ): AsyncReducer<I, O>; /** * Returns an 'AsyncReducer` instance that produces at most `amount` values. * @param amount - the maximum amount of values to produce. */ takeOutput(amount: number): AsyncReducer<I, O>; /** * Returns an 'AsyncReducer` instance that produces until the given `pred` predicate returns true for * the output value. * @param pred - a potaentially asynchronous function that returns true if the value should be passed to the reducer based on the following inputs:<br/> * - value: the current input value<br/> * - index: the current input index<br/> * - halt: function that, when called, ensures no more new values are passed to the reducer * @param options - (optional) an object containing the following properties:<br/> * - negate: (default: false) when true will invert the given predicate */ takeOutputUntil( pred: (value: O, index: number) => MaybePromise<boolean>, options?: { negate?: boolean | undefined } ): AsyncReducer<I, O>; /** * Returns a reducer that applies the given `nextReducers` sequentially after this reducer * has halted, and moving on to the next provided reducer until it is halted. Optionally, it provides the last output * value of the previous reducer. * @param nextReducers - an number of reducers consuming and producing the same types as the current reducer. * @example * ```ts * const result = await AsyncStream.range({ amount: 6 }) * .reduce( * Reducer.sum * .takeInput(3) * .chain( * v => v > 10 ? Reducer.product : Reducer.sum * ) * ) * console.log(result) * // => 21 * ``` */ chain<O2 extends O>( nextReducers: AsyncStreamSource< AsyncOptLazy<AsyncReducer.Accept<I, O2>, [O2]> > ): AsyncReducer<I, O2>; /** * Returns a promise that resolves to a 'runnable' instance of the current reducer specification. This instance maintains its own state * and indices, so that the instance only needs to be provided the input values, and output values can be * retrieved when needed. The state is kept private. * @example * ```ts * const reducer = AsyncReducer.from(Reducer.sum.mapOutput(v => v * 2)); * const instance = reducer.compile(); * await instance.next(3); * await instance.next(5); * console.log(await instance.getOutput()); * // => 16 * ``` */ compile(): Promise<AsyncReducer.Instance<I, O>>; } /** * A base class that can be used to easily create `AsyncReducer` instances. * @typeparam I - the input value type * @typeparam O - the output value type * @typeparam S - the internal state type */ export class Base<I, O, S> implements AsyncReducer.Impl<I, O, S> { constructor( readonly init: (initHalt: () => void) => MaybePromise<S>, readonly next: ( state: S, elem: I, index: number, halt: () => void ) => MaybePromise<S>, readonly stateToResult: ( state: S, index: number, halted: boolean ) => MaybePromise<O>, readonly onClose?: (state: S, error?: unknown) => MaybePromise<void> ) {} filterInput( pred: ( value: I, index: number, halt: () => void ) => MaybePromise<boolean>, options: { negate?: boolean | undefined } = {} ): any { const { negate = false } = options; return create<I, any>( () => this.compile(), async (state, elem, index, halt) => { if ((await pred(elem, index, halt)) !== negate) { await state.next(elem); if (state.halted) { halt(); } } return state; }, (state) => state.getOutput(), (state, err) => state.onClose(err) ); } mapInput<I2>( mapFun: (value: I2, index: number) => MaybePromise<I> ): AsyncReducer<I2, O> { return create( this.init, async (state, elem, index, halt): Promise<S> => this.next(state, await mapFun(elem, index), index, halt), this.stateToResult, this.onClose ); } flatMapInput<I2>( flatMapFun: ( value: I2, index: number ) => MaybePromise<AsyncStreamSource<I>> ): AsyncReducer<I2, O> { return create<I2, O, AsyncReducer.Instance<I, O>>( () => this.compile(), async (state, elem, index, halt) => { if (state.halted) { halt(); return state; } const elems = await flatMapFun(elem, index); const iter = fromAsyncStreamSource(elems)[Symbol.asyncIterator](); const done = Symbol(); let value: I | typeof done; while (done !== (value = await iter.fastNext(done))) { await state.next(value); if (state.halted) { halt(); break; } } return state; }, (state) => state.getOutput(), (state, err) => state.onClose(err) ); } collectInput<I2>(collectFun: AsyncCollectFun<I2, I>): AsyncReducer<I2, O> { return create( () => this.compile(), async (state, elem, index, halt) => { const nextElem = await collectFun(elem, index, CollectFun.Skip, halt); if (CollectFun.Skip !== nextElem) { await state.next(nextElem); if (state.halted) { halt(); } } return state; }, (state) => state.getOutput(), (state, err) => state.onClose(err) ); } mapOutput<O2>( mapFun: (value: O, index: number, halted: boolean) => MaybePromise<O2> ): AsyncReducer<I, O2> { return create( this.init, this.next, async (state, index, halted): Promise<O2> => mapFun(await this.stateToResult(state, index, halted), index, halted), this.onClose ); } takeOutput(amount: number): AsyncReducer<I, O> { if (amount <= 0) { return create( (initHalt) => { initHalt(); return this.init(initHalt); }, this.next, this.stateToResult, this.onClose ); } return create( this.init, (state, next, index, halt) => { if (index >= amount - 1) { halt(); } return this.next(state, next, index, halt); }, this.stateToResult, this.onClose ); } takeOutputUntil( pred: (value: O, index: number) => MaybePromise<boolean>, options: { negate?: boolean } = {} ): AsyncReducer<I, O> { const { negate = false } = options; return create( this.init, async (state, next, index, halt) => { const nextState = await this.next(state, next, index, halt); const nextOutput = await this.stateToResult(nextState, index, false); if ((await pred(nextOutput, index)) !== negate) { halt(); } return nextState; }, this.stateToResult, this.onClose ); } takeInput(amount: number): AsyncReducer<I, O> { if (amount <= 0) { return create(this.init, identity, this.stateToResult, this.onClose); } return this.filterInput((_, i, halt): boolean => { if (i >= amount - 1) { halt(); } return i < amount; }); } dropInput(amount: number): AsyncReducer<I, O> { if (amount <= 0) { return this as AsyncReducer<I, O>; } return this.filterInput((_, i): boolean => i >= amount); } sliceInput(from = 0, amount?: number): AsyncReducer<I, O> { if (undefined === amount) return this.dropInput(from); if (amount <= 0) return create(this.init, identity, this.stateToResult); if (from <= 0) return this.takeInput(amount); return this.takeInput(amount).dropInput(from); } chain<O2 extends O>( nextReducers: AsyncStreamSource< AsyncOptLazy<AsyncReducer.Accept<I, O2>, [O2]> > ): AsyncReducer<I, O2> { return AsyncReducer.create( async ( initHalt ): Promise<{ activeInstance: AsyncReducer.Instance<I, O2>; iterator: AsyncFastIterator< AsyncOptLazy<AsyncReducer.Accept<I, O2>, [O2]> >; }> => { const iterator = fromAsyncStreamSource(nextReducers)[Symbol.asyncIterator](); let activeInstance = (await this.compile()) as AsyncReducer.Instance< I, O2 >; if (undefined !== activeInstance && activeInstance.halted) { let output = await activeInstance.getOutput(); do { const creator = await iterator.fastNext(); if (undefined === creator) { initHalt(); return { activeInstance, iterator, }; } const nextReducer = await AsyncOptLazy.toMaybePromise( creator, output ); activeInstance = await AsyncReducer.from(nextReducer).compile(); output = await activeInstance.getOutput(); } while (activeInstance.halted); } return { activeInstance, iterator, }; }, async (state, next, index, halt) => { await state.activeInstance.next(next); while (state.activeInstance.halted) { const output = await state.activeInstance.getOutput(); const creator = await state.iterator.fastNext(); if (undefined === creator) { halt(); return state; } const nextReducer = await AsyncOptLazy.toMaybePromise( creator, output ); state.activeInstance = await AsyncReducer.from(nextReducer).compile(); } return state; }, (state) => state.activeInstance.getOutput() ); } async compile(): Promise<AsyncReducer.Instance<I, O>> { const instance = new AsyncReducer.InstanceImpl(this); await instance.initialize(); return instance; } } /** * An async reducer instance that manages its own state based on the reducer definition that * was used to create this instance. * @typeparam I - the input element type * @typeparam O - the output element type */ export interface Instance<I, O> { /** * Returns true if the reducer instance does not receive any more values, false otherwise. */ get halted(): boolean; /** * Returns the index of the last received value. */ get index(): number; /** * Method that, when called, halts the reducer instance so that it will no longer receive values. */ halt(): void; /** * Sends a new value into the reducer instance. * @param value - the next input value */ next(value: I): MaybePromise<void>; /** * Returns the output value based on the current given input values. */ getOutput(): MaybePromise<O>; /** * Closes any resources that may have been opened. * @param err - (optional) if an error occurrerd it can be supplied */ onClose(err?: unknown): Promise<void>; } /** * The default `AsyncReducer.Impl` implementation. * @typeparam I - the input element type * @typeparam O - the output element type * @typeparam S - the reducer state type */ export class InstanceImpl<I, O, S> implements AsyncReducer.Instance<I, O> { constructor(readonly reducer: AsyncReducer.Impl<I, O, S>) {} #state: S | undefined; #index = 0; #initialized = false; #halted = false; #closed = false; async initialize(): Promise<void> { if (this.#closed) { throw new AsyncReducer.ReducerClosedError(); } this.#state = await this.reducer.init(this.halt); this.#initialized = true; } halt = (): void => { if (this.#closed) { throw new AsyncReducer.ReducerClosedError(); } this.#halted = true; }; get halted(): boolean { return this.#halted; } get index(): number { return this.#index; } next = async (value: I): Promise<void> => { if (!this.#initialized) { throw new AsyncReducer.ReducerNotInitializedError(); } if (this.#closed) { throw new AsyncReducer.ReducerClosedError(); } if (this.#halted) { throw new AsyncReducer.ReducerHaltedError(); } this.#state = await this.reducer.next( this.#state!, value, this.#index++, this.halt ); }; async getOutput(): Promise<O> { if (!this.#initialized) { throw new AsyncReducer.ReducerNotInitializedError(); } return this.reducer.stateToResult(this.#state!, this.index, this.halted); } async onClose(err?: unknown): Promise<void> { if (this.#closed) { throw new AsyncReducer.ReducerClosedError(); } this.#closed = true; await this.reducer.onClose?.(this.#state!, err); } } /** * Returns an `AsyncReducer` with the given options: * @param init - the optionally lazy and/or promised initial state value * @param next - returns (potentially asynchronously) the next state value based on the given inputs:<br/> * - current: the current state<br/> * - next: the current input value<br/> * - index: the input index value<br/> * - halt: function that, when called, ensures no more elements are passed to the reducer * @param stateToResult - a potentially asynchronous function that converts the current state to an output value * @param onClose - (optional) a function that will be called when the reducer will no longer receive values * @typeparam I - the input value type * @typeparam O - the output value type * @typeparam S - the internal state type */ export function create<I, O = I, S = O>( init: (initHalt: () => void) => MaybePromise<S>, next: ( current: S, next: I, index: number, halt: () => void ) => MaybePromise<S>, stateToResult: ( state: S, index: number, halted: boolean ) => MaybePromise<O>, onClose?: (state: S, error?: unknown) => MaybePromise<void> ): AsyncReducer<I, O> { return new AsyncReducer.Base( init, next, stateToResult, onClose ) as AsyncReducer<I, O>; } /** * Returns an `AsyncReducer` of which the input, state, and output types are the same. * @param init - the optionally lazy and/or promised initial state value * @param next - returns (potentially asynchronously) the next state value based on the given inputs:<br/> * - current: the current state<br/> * - next: the current input value<br/> * - index: the input index value<br/> * - halt: function that, when called, ensures no more elements are passed to the reducer * @param stateToResult - a potentially asynchronous function that converts the current state to an output value * @param onClose - (optional) a function that will be called when the reducer will no longer receive values * @typeparam T - the overall value type */ export function createMono<T>( init: (initHalt: () => void) => MaybePromise<T>, next: ( current: T, next: T, index: number, halt: () => void ) => MaybePromise<T>, stateToResult?: ( state: T, index: number, halted: boolean ) => MaybePromise<T>, onClose?: (state: T, error?: unknown) => MaybePromise<void> ): AsyncReducer<T> { return create(init, next, stateToResult ?? identity, onClose); } /** * Returns an `AsyncReducer` of which the state and output types are the same. * @param init - the optionally lazy and/or promised initial state value * @param next - returns (potentially asynchronously) the next state value based on the given inputs:<br/> * - current: the current state<br/> * - next: the current input value<br/> * - index: the input index value<br/> * - halt: function that, when called, ensures no more elements are passed to the reducer * @param stateToResult - a potentially asynchronous function that converts the current state to an output value * @param onClose - (optional) a function that will be called when the reducer will no longer receive values * @typeparam I - the input value type * @typeparam O - the output value type */ export function createOutput<I, O = I>( init: (initHalt: () => void) => MaybePromise<O>, next: ( current: O, next: I, index: number, halt: () => void ) => MaybePromise<O>, stateToResult?: ( state: O, index: number, halted: boolean ) => MaybePromise<O>, onClose?: (state: O, error?: unknown) => MaybePromise<void> ): AsyncReducer<I, O> { return create(init, next, stateToResult ?? identity, onClose); } /** * Returns an `AsyncReducer` that uses the given `init` and `next` values to fold the input values into * result values. * @param init - an (optionally lazy) initial result value * @param next - a (potentially async) function taking the following arguments:<br/> * - current - the current result value<br/> * - value - the next input value<br/> * - index: the input index value<br/> * - halt: function that, when called, ensures no more elements are passed to the reducer * @typeparam T - the input type * @typeparam R - the output type */ export function fold<T, R>( init: AsyncOptLazy<R>, next: ( current: R, value: T, index: number, halt: () => void ) => MaybePromise<R> ): AsyncReducer<T, R> { return AsyncReducer.createOutput( () => AsyncOptLazy.toMaybePromise(init), next ); } /** * Returns an `AsyncReducer` from a given `Reducer` or `AsyncReducer` instance. * @param reducer - the input reducer to convert * @typeparam I - the input element type * @typeparam O - the output element type */ export function from<I, O>( reducer: AsyncReducer.Accept<I, O> ): AsyncReducer<I, O> { if (reducer instanceof AsyncReducer.Base) { return reducer; } return AsyncReducer.create( reducer.init, reducer.next, reducer.stateToResult as any ); } /** * Returns a `Reducer` that remembers the minimum value of the inputs using the given `compFun` to compare input values * @param compFun - a comparison function for two input values, returning 0 when equal, positive when greater, negetive when smaller * @param otherwise - (default: undefineds) a fallback value when there were no input values given * @typeparam T - the element type * @typeparam O - the fallback value type * @example * ```ts * const stream = Stream.of('abc', 'a', 'abcde', 'ab') * console.log(stream.minBy((s1, s2) => s1.length - s2.length)) * // 'a' * ``` */ export const minBy: { <T>( compFun: (v1: T, v2: T) => MaybePromise<number> ): AsyncReducer<T, T | undefined>; <T, O>( compFun: (v1: T, v2: T) => MaybePromise<number>, otherwise: AsyncOptLazy<O> ): AsyncReducer<T, T | O>; } = <T, O>( compFun: (v1: T, v2: T) => MaybePromise<number>, otherwise?: AsyncOptLazy<O> ) => { const token = Symbol(); return create<T, T | O, T | typeof token>( () => token, async (state, next): Promise<T> => { if (token === state) return next; return (await compFun(state, next)) < 0 ? state : next; }, (state): MaybePromise<T | O> => token === state ? AsyncOptLazy.toMaybePromise(otherwise!) : state ); }; /** * Returns a `Reducer` that remembers the minimum value of the numberic inputs. * @param otherwise - (default: undefined) a fallback value when there were no input values given * @typeparam O - the fallback value type * @example * ```ts * console.log(Stream.of(5, 3, 7, 4).reduce(Reducer.min())) * // => 3 * ``` */ // prettier-ignore export const min: { (): AsyncReducer<number, number | undefined>; <O>(otherwise: AsyncOptLazy<O>): AsyncReducer<number, number | O>; } = <O,>(otherwise?: AsyncOptLazy<O>) => { return create<number, number | O, number | undefined>( () => undefined, (state, next): number => undefined !== state && state < next ? state : next, (state): MaybePromise<number | O> => state ?? AsyncOptLazy.toMaybePromise(otherwise!) ); }; /** * Returns a `Reducer` that remembers the maximum value of the inputs using the given `compFun` to compare input values * @param compFun - a comparison function for two input values, returning 0 when equal, positive when greater, negetive when smaller * @param otherwise - (default: undefined) a fallback value when there were no input values given * @typeparam T - the element type * @typeparam O - the fallback value type * @example * ```ts * const stream = Stream.of('abc', 'a', 'abcde', 'ab') * console.log(stream.maxBy((s1, s2) => s1.length - s2.length)) * // 'abcde' * ``` */ export const maxBy: { <T>( compFun: (v1: T, v2: T) => MaybePromise<number> ): AsyncReducer<T, T | undefined>; <T, O>( compFun: (v1: T, v2: T) => MaybePromise<number>, otherwise: AsyncOptLazy<O> ): AsyncReducer<T, T | O>; } = <T, O>( compFun: (v1: T, v2: T) => MaybePromise<number>, otherwise?: AsyncOptLazy<O> ): AsyncReducer<T, T | O> => { const token = Symbol(); return create<T, T | O, T | typeof token>( () => token, async (state, next): Promise<T> => { if (token === state) return next; return (await compFun(state, next)) > 0 ? state : next; }, (state): MaybePromise<T | O> => token === state ? AsyncOptLazy.toMaybePromise(otherwise!) : state ); }; /** * Returns a `Reducer` that remembers the maximum value of the numberic inputs. * @param otherwise - (default: undefined) a fallback value when there were no input values given * @typeparam O - the fallback value type * @example * ```ts * console.log(Stream.of(5, 3, 7, 4).reduce(Reducer.max())) * // => 7 * ``` */ // prettier-ignore export const max: { (): AsyncReducer<number, number | undefined>; <O>(otherwise: AsyncOptLazy<O>): AsyncReducer<number, number | O>; } = <O,>(otherwise?: AsyncOptLazy<O>): AsyncReducer<number, number | O> => { return create<number, number | O, number | undefined>( () => undefined, (state, next): number => undefined !== state && state > next ? state : next, (state): MaybePromise<number | O> => state ?? AsyncOptLazy.toMaybePromise(otherwise!) ); }; /** * Returns an `AsyncReducer` that remembers the first input value. * @param otherwise - (default: undefined) a fallback value to output if no input value has been provided * @typeparam T - the input value type * @typeparam O - the fallback value type * @example * ```ts * await AsyncStream.from(Stream.range{ amount: 10 })).reduce(AsyncReducer.first()) * // => 0 * ``` */ export const first: { <T>(): AsyncReducer<T, T | undefined>; <T, O>(otherwise: AsyncOptLazy<O>): AsyncReducer<T, T | O>; } = <T, O>(otherwise?: AsyncOptLazy<O>): AsyncReducer<T, T | O> => { return create<T, T | O, T | undefined>( () => undefined, (state, next, _, halt): T => { halt(); return next; }, (state, index): MaybePromise<T | O> => index <= 0 ? AsyncOptLazy.toMaybePromise(otherwise!) : state! ); }; /** * Returns an `AsyncReducer` that remembers the last input value. * @param otherwise - (default: undefined) a fallback value to output if no input value has been provided * @typeparam T - the input value type * @typeparam O - the fallback value type * @example * ```ts * await AsyncStream.from(Stream.range{ amount: 10 })).reduce(AsyncReducer.last()) * // => 9 * ``` */ export const last: { <T>(): AsyncReducer<T, T | undefined>; <T, O>(otherwise: AsyncOptLazy<O>): AsyncReducer<T, T | O>; } = <T, O>(otherwise?: AsyncOptLazy<O>): AsyncReducer<T, T | O> => { return create<T, T | O, T | undefined>( () => undefined, (_, next): T => next, (state, index): MaybePromise<T | O> => index <= 0 ? AsyncOptLazy.toMaybePromise(otherwise!) : state! ); }; /** * Returns an AsyncReducer that only produces an output value when having receives exactly one * input value, otherwise will return the `otherwise` value or undefined. * @param otherwise - the fallback value to return when more or less than one value is received. * @typeparam T - the element type * @typeparam O - the fallback value type */ export const single: { <T>(): AsyncReducer<T, T | undefined>; <T, O>(otherwise: AsyncOptLazy<O>): AsyncReducer<T, T | O>; } = <T, O>(otherwise?: AsyncOptLazy<O>): AsyncReducer<T, T | O> => { return create<T, T | O, T | undefined>( () => undefined, (state, next, index, halt): T => { if (index > 1) { halt(); } return next; }, (state, index): MaybePromise<T | O> => index !== 1 ? AsyncOptLazy.toMaybePromise(otherwise!) : state! ); }; /** * Returns an `AsyncReducer` that ouputs false as long as no input value satisfies given `pred`, true otherwise. * @typeparam T - the element type * @param pred - a potentiall async function taking an input value and its index, and returning true if the value satisfies the predicate * @param options - (optional) an object containing the following properties:<br/> * - negate: (default: false) when true will invert the given predicate */ export function some<T>( pred: (value: T, index: number) => MaybePromise<boolean>, options: { negate?: boolean | undefined } = {} ): AsyncReducer<T, boolean> { return nonEmpty.filterInput(pred, options); } /** * Returns an `AsyncReducer` that ouputs true as long as all input values satisfy the given `pred`, false otherwise. * @typeparam T - the element type * @param pred - a potentially async function taking an input value and its index, and returning true if the value satisfies the predicate * @param options - (optional) an object containing the following properties:<br/> * - negate: (default: false) when true will invert the given predicate */ export function every<T>( pred: (value: T, index: number) => MaybePromise<boolean>, options: { negate?: boolean | undefined } = {} ): AsyncReducer<T, boolean> { const { negate = false } = options; return isEmpty.filterInput(pred, { negate: !negate }); } /** * Returns an `AsyncReducer` that ouputs true when the received elements match the given `other` async stream source according to the `eq` instance, false otherwise. * @typeparam T - the element type * @param other - an async stream source containg elements to match against * @param options - (optional) an object containing the following properties:<br/> * - eq: (default: Eq.objectIs) the `Eq` instance to use to compare elements * - negate: (default: false) when true will invert the given predicate */ export function equals<T>( other: AsyncStreamSource<T>, options: { eq?: Eq<T> | undefined; negate?: boolean | undefined } = {} ): AsyncReducer<T, boolean> { const { eq = Eq.objectIs, negate = false } = options; const sliceStream = fromAsyncStreamSource(other); const done = Symbol(); return AsyncReducer.create< T, boolean, { iter: AsyncFastIterator<T>; nextSeq: T | typeof done; result: boolean } >( async () => { const iter = sliceStream[Symbol.asyncIterator](); const nextSeq = await iter.fastNext(done); return { iter, nextSeq, result: false }; }, async (state, next, _, halt) => { if (done === state.nextSeq) { halt(); state.result = false; return state; } if (eq(next, state.nextSeq) === negate) { halt(); state.result = false; return state; } state.nextSeq = await state.iter.fastNext(done); if (done === state.nextSeq) { state.result = true; } return state; }, (state, index, halted) => !halted && done === state.nextSeq ); } /** * An `AsyncReducer` that outputs true if no input values are received, false otherwise. * @example * ```ts * await AsyncStream.of(1, 2, 3).reduce(AsyncReducer.isEmpty)) * // => false * ``` */ export const isEmpty = createOutput<any, boolean>( () => true, (_, __, ___, halt): false => { halt(); return false; } ); /** * An `AsyncReducer` that outputs true if one or more input values are received, false otherwise. * @example * ```ts * await AsyncStream.of(1, 2, 3).reduce(AsyncReducer.nonEmpty)) * // => true * ``` */ export const nonEmpty = createOutput<any, boolean>( () => false, (_, __, ___, halt): true => { halt(); return true; } ); /** * Returns a `AsyncReducer` that returns true if the first input values match the given `slice` values repeated `amount` times. Otherwise, * returns false. * @param slice - a async sequence of elements to match against * @param options - (optional) an object containing the following properties:<br/> * - amount: (detaulf: 1) the amount of elements to find * - eq: (default: Eq.objectIs) the `Eq` instance to use to compare elements */ export function startsWithSlice<T>( slice: AsyncStreamSource<T>, options: { eq?: Eq<T> | undefined; amount?: number } = {} ): AsyncReducer<T, boolean> { const sliceStream = fromAsyncStreamSource(slice); const done = Symbol(); const { eq = Eq.objectIs, amount = 1 } = options; return AsyncReducer.create< T, boolean, { sliceIter: AsyncFastIterator<T>; sliceValue: T | typeof done; remain: number; } >( async (initHalt) => { const sliceIter = sliceStream[Symbol.asyncIterator](); const sliceValue = await sliceIter.fastNext(done); if (done === sliceValue || amount <= 0) { initHalt(); return { sliceIter, sliceValue, remain: 0 }; } return { sliceIter, sliceValue, remain: amount, }; }, async (state, next, _, halt) => { if (done === state.sliceValue) { RimbuError.throwInvalidStateError(); } if (eq(next, state.sliceValue)) { state.sliceValue = await state.sliceIter.fastNext(done); if (done === state.sliceValue) { state.remain--; if (state.remain <= 0) { halt(); } else { state.sliceIter = sliceStream[Symbol.asyncIterator](); state.sliceValue = await state.sliceIter.fastNext(done); } } } else { halt(); } return state; }, (state) => state.remain <= 0 ); } /** * Returns an `AsyncReducer` that returns true if the last input values match the given `slice` values repeated `amount` times. Otherwise, * returns false. * @param slice - a async sequence of elements to match against * @param options - (optional) an object containing the following properties:<br/> * - amount: (detaulf: 1) the amount of elements to find * - eq: (default: Eq.objectIs) the `Eq` instance to use to compare elements */ export function endsWithSlice<T>( slice: AsyncStreamSource<T>, options: { eq?: Eq<T> | undefined; amount?: number } = {} ): AsyncReducer<T, boolean> { const sliceStream = AsyncStreamConstructorsImpl.from(slice); const done = Symbol(); const newReducerSpec = AsyncReducer.startsWithSlice(slice, options); return AsyncReducer.create< T, boolean, Set<AsyncReducer.Instance<T, boolean>> >( async (initHalt) => { const sliceIter = sliceStream[Symbol.asyncIterator](); const sliceValue = await sliceIter.fastNext(done); if (done === sliceValue) { initHalt(); } return new Set([await newReducerSpec.compile()]); }, async (state, nextValue) => { for (const instance of state) { if (instance.halted) { state.delete(instance); } else { await instance.next(nextValue); } } const newReducerInstance = await newReducerSpec.compile(); await newReducerInstance.next(nextValue); state.add(newReducerInstance); return state; }, (state) => state.size === 0 || AsyncStreamConstructorsImpl.from(state).some((instance) => instance.getOutput() ) ); } /** * Returns an `AsyncReducer` that returns true if the input values contain the given `slice` sequence `amount` times. Otherwise, * returns false. * @param slice - a async sequence of elements to match against * @param options - (optional) an object containing the following properties:<br/> * - amount: (detaulf: 1) the amount of elements to find * - eq: (default: Eq.objectIs) the `Eq` instance to use to compare elements */ export function containsSlice<T>( slice: AsyncStreamSource<T>, options: { eq?: Eq<T> | undefined; amount?: number | undefined } = {} ): AsyncReducer<T, boolean> { const { eq, amount = 1 } = options; return AsyncReducer.pipe( endsWithSlice(slice, { eq }), Reducer.contains(true, { amount }) ); } /** * Returns an `AsyncReducer` that splits the incoming values into two separate outputs based on the given `pred` predicate. Values for which the predicate is true * are fed into the `collectorTrue` reducer, and other values are fed into the `collectorFalse` instance. If no collectors are provided the values are collected * into arrays. * @param pred - a potentially async predicate receiving the value and its index * @param options - (optional) an object containing the following properties:<br/> * - collectorTrue: (default: Reducer.toArray()) a reducer that collects the values for which the predicate is true<br/> * - collectorFalse: (default: Reducer.toArray()) a reducer that collects the values for which the predicate is false * @typeparam T - the input element type * @typeparam RT - the reducer result type for the `collectorTrue` value * @typeparam RF - the reducer result type for the `collectorFalse` value * @note if the predicate is a type guard, the return type is automatically inferred * ``` */ export const partition: { <T, T2 extends T, RT, RF = RT>( pred: (value: T, index: number) => value is T2, options: { collectorTrue: AsyncReducer.Accept<T2, RT>; collectorFalse: AsyncReducer.Accept<Exclude<T, T2>, RF>; } ): AsyncReducer<T, [true: RT, false: RF]>; <T, T2 extends T>( pred: (value: T, index: number) => value is T2, options?: { collectorTrue?: undefined; collectorFalse?: undefined; } ): AsyncReducer<T, [true: T2[], false: Exclude<T, T2>[]]>; <T, RT, RF = RT>( pred: (value: T, index: number) => MaybePromise<boolean>, options: { collectorTrue: AsyncReducer.Accept<T, RT>; collectorFalse: AsyncReducer.Accept<T, RF>; } ): AsyncReducer<T, [true: RT, false: RF]>; <T>( pred: (value: T, index: number) => MaybePromise<boolean>, options?: { collectorTrue?: undefined; collectorFalse?: undefined; } ): AsyncReducer<T, [true: T[], false: T[]]>; } = <T, RT, RF = RT>( pred: (value: T, index: number) => MaybePromise<boolean>, options: { collectorTrue?: any; collectorFalse?: any; } = {} ): AsyncReducer<T, [true: RT, false: RF]> => { const { collectorTrue = Reducer.toArray() as AsyncReducer.Accept<T, RT>, collectorFalse = Reducer.toArray() as AsyncReducer.Accept<T, RF>, } = options; return AsyncReducer.create( () => Promise.all([ AsyncReducer.from(collectorTrue).compile(), AsyncReducer.from(collectorFalse).compile(), ]), async (state, value, index) => { const instanceIndex = (await pred(value, index)) ? 0 : 1; await state[instanceIndex].next(value); return state; }, (state) => Promise.all( Stream.from(state).mapPure((v) => v.getOutput()) ) as Promise<[RT, RF]> ); }; /** * Returns an `AsyncReducer` that uses the `valueToKey` function to calculate a key for each value, and feeds the tuple of the k