@rimbu/stream
Version:
Efficient structure representing a sequence of elements, with powerful operations for TypeScript
873 lines • 40.9 kB
JavaScript
import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib";
import { RimbuError } from '@rimbu/base';
import { AsyncOptLazy, CollectFun, Eq, ErrBase, } from '@rimbu/common';
import { Reducer, Stream, } from '@rimbu/stream';
import { AsyncStreamConstructorsImpl, fromAsyncStreamSource, } from '../async-custom/async-stream-custom.mjs';
function identity(value) {
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(...reducers) {
return AsyncReducer.create(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(reducerObj) {
return AsyncReducer.create(async (initHalt) => {
const result = {};
let allHalted = true;
await Promise.all(Stream.fromObject(reducerObj).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 = {};
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 var AsyncReducer;
(function (AsyncReducer) {
var _InstanceImpl_state, _InstanceImpl_index, _InstanceImpl_initialized, _InstanceImpl_halted, _InstanceImpl_closed;
/**
* 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
*/
class Base {
constructor(init, next, stateToResult, onClose) {
this.init = init;
this.next = next;
this.stateToResult = stateToResult;
this.onClose = onClose;
}
filterInput(pred, options = {}) {
const { negate = false } = options;
return create(() => 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(mapFun) {
return create(this.init, async (state, elem, index, halt) => this.next(state, await mapFun(elem, index), index, halt), this.stateToResult, this.onClose);
}
flatMapInput(flatMapFun) {
return create(() => 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;
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(collectFun) {
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(mapFun) {
return create(this.init, this.next, async (state, index, halted) => mapFun(await this.stateToResult(state, index, halted), index, halted), this.onClose);
}
takeOutput(amount) {
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, options = {}) {
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) {
if (amount <= 0) {
return create(this.init, identity, this.stateToResult, this.onClose);
}
return this.filterInput((_, i, halt) => {
if (i >= amount - 1) {
halt();
}
return i < amount;
});
}
dropInput(amount) {
if (amount <= 0) {
return this;
}
return this.filterInput((_, i) => i >= amount);
}
sliceInput(from = 0, amount) {
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(nextReducers) {
return AsyncReducer.create(async (initHalt) => {
const iterator = fromAsyncStreamSource(nextReducers)[Symbol.asyncIterator]();
let activeInstance = (await this.compile());
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() {
const instance = new AsyncReducer.InstanceImpl(this);
await instance.initialize();
return instance;
}
}
AsyncReducer.Base = Base;
/**
* The default `AsyncReducer.Impl` implementation.
* @typeparam I - the input element type
* @typeparam O - the output element type
* @typeparam S - the reducer state type
*/
class InstanceImpl {
constructor(reducer) {
this.reducer = reducer;
_InstanceImpl_state.set(this, void 0);
_InstanceImpl_index.set(this, 0);
_InstanceImpl_initialized.set(this, false);
_InstanceImpl_halted.set(this, false);
_InstanceImpl_closed.set(this, false);
this.halt = () => {
if (__classPrivateFieldGet(this, _InstanceImpl_closed, "f")) {
throw new AsyncReducer.ReducerClosedError();
}
__classPrivateFieldSet(this, _InstanceImpl_halted, true, "f");
};
this.next = async (value) => {
var _a, _b;
if (!__classPrivateFieldGet(this, _InstanceImpl_initialized, "f")) {
throw new AsyncReducer.ReducerNotInitializedError();
}
if (__classPrivateFieldGet(this, _InstanceImpl_closed, "f")) {
throw new AsyncReducer.ReducerClosedError();
}
if (__classPrivateFieldGet(this, _InstanceImpl_halted, "f")) {
throw new AsyncReducer.ReducerHaltedError();
}
__classPrivateFieldSet(this, _InstanceImpl_state, await this.reducer.next(__classPrivateFieldGet(this, _InstanceImpl_state, "f"), value, (__classPrivateFieldSet(this, _InstanceImpl_index, (_b = __classPrivateFieldGet(this, _InstanceImpl_index, "f"), _a = _b++, _b), "f"), _a), this.halt), "f");
};
}
async initialize() {
if (__classPrivateFieldGet(this, _InstanceImpl_closed, "f")) {
throw new AsyncReducer.ReducerClosedError();
}
__classPrivateFieldSet(this, _InstanceImpl_state, await this.reducer.init(this.halt), "f");
__classPrivateFieldSet(this, _InstanceImpl_initialized, true, "f");
}
get halted() {
return __classPrivateFieldGet(this, _InstanceImpl_halted, "f");
}
get index() {
return __classPrivateFieldGet(this, _InstanceImpl_index, "f");
}
async getOutput() {
if (!__classPrivateFieldGet(this, _InstanceImpl_initialized, "f")) {
throw new AsyncReducer.ReducerNotInitializedError();
}
return this.reducer.stateToResult(__classPrivateFieldGet(this, _InstanceImpl_state, "f"), this.index, this.halted);
}
async onClose(err) {
if (__classPrivateFieldGet(this, _InstanceImpl_closed, "f")) {
throw new AsyncReducer.ReducerClosedError();
}
__classPrivateFieldSet(this, _InstanceImpl_closed, true, "f");
await this.reducer.onClose?.(__classPrivateFieldGet(this, _InstanceImpl_state, "f"), err);
}
}
_InstanceImpl_state = new WeakMap(), _InstanceImpl_index = new WeakMap(), _InstanceImpl_initialized = new WeakMap(), _InstanceImpl_halted = new WeakMap(), _InstanceImpl_closed = new WeakMap();
AsyncReducer.InstanceImpl = InstanceImpl;
/**
* 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
*/
function create(init, next, stateToResult, onClose) {
return new AsyncReducer.Base(init, next, stateToResult, onClose);
}
AsyncReducer.create = create;
/**
* 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
*/
function createMono(init, next, stateToResult, onClose) {
return create(init, next, stateToResult ?? identity, onClose);
}
AsyncReducer.createMono = createMono;
/**
* 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
*/
function createOutput(init, next, stateToResult, onClose) {
return create(init, next, stateToResult ?? identity, onClose);
}
AsyncReducer.createOutput = createOutput;
/**
* 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
*/
function fold(init, next) {
return AsyncReducer.createOutput(() => AsyncOptLazy.toMaybePromise(init), next);
}
AsyncReducer.fold = fold;
/**
* 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
*/
function from(reducer) {
if (reducer instanceof AsyncReducer.Base) {
return reducer;
}
return AsyncReducer.create(reducer.init, reducer.next, reducer.stateToResult);
}
AsyncReducer.from = from;
/**
* 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'
* ```
*/
AsyncReducer.minBy = (compFun, otherwise) => {
const token = Symbol();
return create(() => token, async (state, next) => {
if (token === state)
return next;
return (await compFun(state, next)) < 0 ? state : next;
}, (state) => 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
AsyncReducer.min = (otherwise) => {
return create(() => undefined, (state, next) => undefined !== state && state < next ? state : next, (state) => 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'
* ```
*/
AsyncReducer.maxBy = (compFun, otherwise) => {
const token = Symbol();
return create(() => token, async (state, next) => {
if (token === state)
return next;
return (await compFun(state, next)) > 0 ? state : next;
}, (state) => 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
AsyncReducer.max = (otherwise) => {
return create(() => undefined, (state, next) => undefined !== state && state > next ? state : next, (state) => 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
* ```
*/
AsyncReducer.first = (otherwise) => {
return create(() => undefined, (state, next, _, halt) => {
halt();
return next;
}, (state, index) => 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
* ```
*/
AsyncReducer.last = (otherwise) => {
return create(() => undefined, (_, next) => next, (state, index) => 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
*/
AsyncReducer.single = (otherwise) => {
return create(() => undefined, (state, next, index, halt) => {
if (index > 1) {
halt();
}
return next;
}, (state, index) => 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
*/
function some(pred, options = {}) {
return AsyncReducer.nonEmpty.filterInput(pred, options);
}
AsyncReducer.some = some;
/**
* 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
*/
function every(pred, options = {}) {
const { negate = false } = options;
return AsyncReducer.isEmpty.filterInput(pred, { negate: !negate });
}
AsyncReducer.every = every;
/**
* 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
*/
function equals(other, options = {}) {
const { eq = Eq.objectIs, negate = false } = options;
const sliceStream = fromAsyncStreamSource(other);
const done = Symbol();
return AsyncReducer.create(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);
}
AsyncReducer.equals = equals;
/**
* 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
* ```
*/
AsyncReducer.isEmpty = createOutput(() => true, (_, __, ___, halt) => {
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
* ```
*/
AsyncReducer.nonEmpty = createOutput(() => false, (_, __, ___, halt) => {
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
*/
function startsWithSlice(slice, options = {}) {
const sliceStream = fromAsyncStreamSource(slice);
const done = Symbol();
const { eq = Eq.objectIs, amount = 1 } = options;
return AsyncReducer.create(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);
}
AsyncReducer.startsWithSlice = startsWithSlice;
/**
* 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
*/
function endsWithSlice(slice, options = {}) {
const sliceStream = AsyncStreamConstructorsImpl.from(slice);
const done = Symbol();
const newReducerSpec = AsyncReducer.startsWithSlice(slice, options);
return AsyncReducer.create(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()));
}
AsyncReducer.endsWithSlice = endsWithSlice;
/**
* 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
*/
function containsSlice(slice, options = {}) {
const { eq, amount = 1 } = options;
return AsyncReducer.pipe(endsWithSlice(slice, { eq }), Reducer.contains(true, { amount }));
}
AsyncReducer.containsSlice = containsSlice;
/**
* 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
* ```
*/
AsyncReducer.partition = (pred, options = {}) => {
const { collectorTrue = Reducer.toArray(), collectorFalse = Reducer.toArray(), } = 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())));
};
/**
* Returns an `AsyncReducer` that uses the `valueToKey` function to calculate a key for each value, and feeds the tuple of the key and the value to the
* `collector` reducer. Finally, it returns the output of the `collector`. If no collector is given, the default collector will return a JS multimap
* of the type `Map<K, V[]>`.
* @param valueToKey - potentially async function taking a value and its index, and returning the corresponding key
* @param options - (optional) an object containing the following properties:<br/>
* - collector: (default: Reducer.toArray()) a reducer that collects the incoming tuple of key and value, and provides the output
* @typeparam T - the input value type
* @typeparam K - the key type
* @typeparam R - the collector output type
* ```
*/
AsyncReducer.groupBy = (valueToKey, options = {}) => {
const { collector = Reducer.toJSMultiMap(), } = options;
return AsyncReducer.create(() => AsyncReducer.from(collector).compile(), async (state, value, index) => {
const key = await valueToKey(value, index);
await state.next([key, value]);
return state;
}, (state) => state.getOutput());
};
/**
* Returns an `AsyncReducer` that feeds incoming values to all reducers in the provided `reducers` source, and halts when the first
* reducer in the array is halted and returns the output of that reducer. Returns the `otherwise` value if no reducer is yet halted.
* @param reducers - a stream source of async reducers that will receive the incoming values
* @param otherwise - a fallback value to return if none of the reducers has been halted
* @typeparam T - the input value type
* @typeparam R - the output value type
* @typeparam O - the fallback value type
*/
AsyncReducer.race = (reducers, otherwise) => {
return AsyncReducer.create(async (initHalt) => {
const instances = await Promise.all(Stream.from(reducers).mapPure((reducer) => AsyncReducer.from(reducer).compile()));
const doneInstance = instances.find((instance) => instance.halted);
if (undefined !== doneInstance) {
initHalt();
}
return { instances, doneInstance };
}, async (state, next, _, halt) => {
for (const instance of state.instances) {
await instance.next(next);
if (instance.halted) {
state.doneInstance = instance;
halt();
return state;
}
}
return state;
}, (state) => state.doneInstance === undefined
? AsyncOptLazy.toMaybePromise(otherwise)
: state.doneInstance.getOutput());
};
class InvalidCombineShapeError extends ErrBase.CustomError {
constructor() {
super('Invalid reducer combine shape supplied');
}
}
AsyncReducer.InvalidCombineShapeError = InvalidCombineShapeError;
class ReducerHaltedError extends ErrBase.CustomError {
constructor() {
super('A halted reducer cannot receive more values');
}
}
AsyncReducer.ReducerHaltedError = ReducerHaltedError;
class ReducerClosedError extends ErrBase.CustomError {
constructor() {
super('A closed async reducer cannot perform more actions');
}
}
AsyncReducer.ReducerClosedError = ReducerClosedError;
class ReducerNotInitializedError extends ErrBase.CustomError {
constructor() {
super('The async reducer instance was not yet initialized');
}
}
AsyncReducer.ReducerNotInitializedError = ReducerNotInitializedError;
/**
* Returns an `AsyncReducer` that combines multiple input `reducers` according to the given "shape" by providing input values to all of them and collecting the outputs in the shape.
* @typeparam T - the input value type for all the reducers
* @typeparam S - the desired result shape type
* @param shape - a shape defining where reducer outputs will be located in the result. It can consist of a single reducer, an array of shapes, or an object with string keys and shapes as values.
*/
function combine(shape) {
if (shape instanceof AsyncReducer.Base) {
return shape;
}
if (shape instanceof Reducer.Base) {
return AsyncReducer.from(shape);
}
if (Array.isArray(shape)) {
return combineArr(...shape.map((item) => AsyncReducer.combine(item)));
}
if (typeof shape === 'object' && shape !== null) {
const result = {};
for (const key in shape) {
result[key] = AsyncReducer.combine(shape[key]);
}
return combineObj(result);
}
throw new AsyncReducer.InvalidCombineShapeError();
}
AsyncReducer.combine = combine;
/**
* Returns an `AsyncReducer` instance that first applies this reducer, and then applies the given `next` reducer to each output produced
* by the previous reducer.
* @typeparam I - the input type of the `reducer1` reducer
* @typeparam O1 - the output type of the `reducer1` reducer
* @typeparam O2 - the output type of the `reducer2` reducer
* @typeparam O3 - the output type of the `reducer3` reducer
* @typeparam O4 - the output type of the `reducer4` reducer
* @typeparam O5 - the output type of the `reducer5` reducer
* @param reducer1 - the next reducer to apply to each output of this reducer.
* @param reducer2 - (optional) the next reducer to apply to each output of this reducer.
* @param reducer3 - (optional) the next reducer to apply to each output of this reducer.
* @param reducer4 - (optional) the next reducer to apply to each output of this reducer.
* @param reducer5 - (optional) the next reducer to apply to each output of this reducer.
* @example
* ```ts
* AsyncStream
* .from(Stream.of(1, 2, 3))
* .reduce(
* AsyncReducer.pipe(Reducer.product, Reducer.sum)
* )
* // => 9
* ```
*/
AsyncReducer.pipe = (...nextReducers) => {
if (nextReducers.length < 2) {
RimbuError.throwInvalidUsageError('Reducer.pipe should have at least two arguments');
}
const [current, next, ...others] = nextReducers;
if (others.length > 0) {
return AsyncReducer.pipe(current, AsyncReducer.pipe(next, ...others));
}
return AsyncReducer.create(async (inithalt) => {
const currentInstance = await AsyncReducer.from(current).compile();
const nextInstance = await AsyncReducer.from(next).compile();
if (currentInstance.halted || nextInstance.halted) {
inithalt();
}
return {
currentInstance,
nextInstance,
};
}, async (state, next, index, halt) => {
const { currentInstance, nextInstance } = state;
await currentInstance.next(next);
await nextInstance.next(await currentInstance.getOutput());
if (currentInstance.halted || nextInstance.halted) {
halt();
}
return state;
}, async (state, index, halted) => {
if (halted && index === 0) {
await state.nextInstance.next(await state.currentInstance.getOutput());
}
return state.nextInstance.getOutput();
}, async (state, err) => {
await Promise.all([
state.currentInstance.onClose(err),
state.nextInstance.onClose(err),
]);
});
};
})(AsyncReducer || (AsyncReducer = {}));
//# sourceMappingURL=async-reducer.mjs.map