sflow
Version:
sflow is a powerful and highly-extensible library designed for processing and manipulating streams of data effortlessly. Inspired by the functional programming paradigm, it provides a rich set of utilities for transforming streams, including chunking, fil
193 lines (180 loc) • 7.08 kB
text/typescript
import DIE from "phpdie";
import { sortBy, type Ord } from "rambda";
import { toStream } from "./froms";
import { sflow, type FlowSource } from "./index";
interface MergeBy {
<T>(ordFn: (input: T) => Ord, srcs: FlowSource<FlowSource<T>>): sflow<T>;
<T>(ordFn: (input: T) => Ord): {
(srcs: FlowSource<FlowSource<T>>): sflow<T>;
};
}
/**
* merge multiple stream by ascend order, assume all input stream is sorted by ascend
* output stream will be sorted by ascend too.
*
* if one of input stream is not sorted by ascend, it will throw an error.
*
* @param ordFn a function to get the order of input
* @param srcs a list of input stream
* @returns a new stream that merge all input stream by ascend order
* @deprecated use {@link mergeStreamsByAscend}, this will be removed in next major version
*/
export const mergeAscends: MergeBy = <T>(
ordFn: (input: T) => Ord,
_srcs?: FlowSource<FlowSource<T>>,
) => {
if (!_srcs) return ((srcs: any) => mergeAscends(ordFn, srcs)) as any;
return sflow(
new ReadableStream<T>(
{
pull: async (ctrl) => {
const srcs = await sflow(_srcs).toArray();
const slots = srcs.map(() => undefined as { value: T } | undefined);
const pendingSlotRemoval = srcs.map(
() => undefined as PromiseWithResolvers<void> | undefined,
);
const drains = srcs.map(() => false);
let lastMinValue: T | undefined = undefined;
await Promise.all(
srcs.map(async (src, i) => {
for await (const value of sflow(src)) {
while (slots[i] !== undefined) {
if (shiftMinValueIfFull()) continue;
pendingSlotRemoval[i] = Promise.withResolvers<void>();
await pendingSlotRemoval[i]!.promise; // wait for this slot empty;
}
slots[i] = { value };
shiftMinValueIfFull();
}
// done
drains[i] = true;
pendingSlotRemoval.map((e) => e?.resolve());
await Promise.all(pendingSlotRemoval.map((e) => e?.promise));
const allDrain = drains.every(Boolean);
if (allDrain) {
while (slots.some((e) => e !== undefined))
shiftMinValueIfFull();
ctrl.close();
}
function shiftMinValueIfFull() {
const isFull = slots.every(
(slot, i) => slot !== undefined || drains[i],
);
if (!isFull) return false;
const fullSlots = slots
.flatMap((e) => (e !== undefined ? [e] : []))
.map((e) => e.value);
const minValue = sortBy(ordFn, fullSlots)[0];
const minIndex = slots.findIndex((e) => e?.value === minValue);
if (lastMinValue !== undefined) {
const ordered = sortBy(ordFn, [lastMinValue, minValue]);
(ordered[0] === lastMinValue && ordered[1] === minValue) ||
DIE(`
MergeAscendError: one of source stream is not ascending ordered.
stream index: ${minIndex}
prev: ${ordFn(lastMinValue)}
prev: ${JSON.stringify(lastMinValue)}
curr: ${ordFn(minValue)}
curr: ${JSON.stringify(minValue)}
`);
}
lastMinValue = minValue;
ctrl.enqueue(minValue);
slots[minIndex] = undefined;
pendingSlotRemoval[minIndex]?.resolve();
pendingSlotRemoval[minIndex] = undefined;
return true;
}
}),
);
},
},
{ highWaterMark: 0 },
),
);
};
/**
* merge multiple stream by ascend order, assume all input stream is sorted by ascend
* output stream will be sorted by ascend too.
*
* if one of input stream is not sorted by ascend, it will throw an error.
*
* @param ordFn a function to get the order of input
* @param srcs a list of input stream
* @returns a new stream that merge all input stream by ascend order
* @deprecated use {@link mergeStreamsByAscend}
*/
export const mergeDescends: MergeBy = <T>(
ordFn: (input: T) => Ord,
_srcs?: FlowSource<FlowSource<T>>,
) => {
if (!_srcs) return ((srcs: any) => mergeDescends(ordFn, srcs)) as any;
return toStream(
new ReadableStream<T>(
{
pull: async (ctrl) => {
const srcs = await sflow(_srcs).toArray();
const slots = srcs.map(() => undefined as { value: T } | undefined);
const pendingSlotRemoval = srcs.map(
() => undefined as PromiseWithResolvers<void> | undefined,
);
const drains = srcs.map(() => false);
let lastMaxValue: T | undefined = undefined;
await Promise.all(
srcs.map(async (src, i) => {
for await (const value of toStream(src)) {
while (slots[i] !== undefined) {
if (shiftMaxValueIfFull()) continue;
pendingSlotRemoval[i] = Promise.withResolvers<void>();
await pendingSlotRemoval[i]!.promise; // wait for this slot empty;
}
slots[i] = { value };
shiftMaxValueIfFull();
}
// done
drains[i] = true;
pendingSlotRemoval.map((e) => e?.resolve());
await Promise.all(pendingSlotRemoval.map((e) => e?.promise));
const allDrain = drains.every(Boolean);
if (allDrain) {
while (slots.some((e) => e !== undefined))
shiftMaxValueIfFull();
ctrl.close();
}
function shiftMaxValueIfFull() {
const isFull = slots.every(
(slot, i) => slot !== undefined || drains[i],
);
if (!isFull) return false;
const fullSlots = slots
.flatMap((e) => (e !== undefined ? [e] : []))
.map((e) => e.value);
const maxValue = sortBy(ordFn, fullSlots).toReversed()[0];
const maxIndex = slots.findIndex((e) => e?.value === maxValue);
if (lastMaxValue !== undefined) {
const ordered = sortBy(ordFn, [maxValue, lastMaxValue]);
(ordered[0] === maxValue && ordered[1] === lastMaxValue) ||
DIE(`
MergeDescendError: one of source stream is not descending ordered.
stream index: ${maxIndex}
prev: ${ordFn(lastMaxValue)}
prev: ${JSON.stringify(lastMaxValue)}
curr: ${ordFn(maxValue)}
curr: ${JSON.stringify(maxValue)}
`);
}
lastMaxValue = maxValue;
ctrl.enqueue(maxValue);
slots[maxIndex] = undefined;
pendingSlotRemoval[maxIndex]?.resolve();
pendingSlotRemoval[maxIndex] = undefined;
return true;
}
}),
);
},
},
{ highWaterMark: 0 },
),
);
};