Files
suaveplan/packages/foundation/flow/streams/src/pipeline/merge/merge.ts
2026-05-27 23:47:53 +01:00

78 lines
2.3 KiB
TypeScript

/**
* Merges multiple ReadableStreams into one.
* Chunks are emitted in arrival order (race-based). The merged stream closes
* when all input streams have closed.
*/
export function merge<T>(streams: ReadableStream<T>[]): ReadableStream<T> {
if (streams.length === 0) {
return new ReadableStream({
start(c) {
c.close();
},
});
}
if (streams.length === 1) {
return streams[0]!;
}
const readers = streams.map((s) => s.getReader());
let closedCount = 0;
// Pull promises: one per reader
const pulling = new Map<
ReadableStreamDefaultReader<T>,
Promise<{
reader: ReadableStreamDefaultReader<T>;
result: ReadableStreamReadResult<T>;
}>
>();
function pullFrom(reader: ReadableStreamDefaultReader<T>): Promise<{
reader: ReadableStreamDefaultReader<T>;
result: ReadableStreamReadResult<T>;
}> {
const p = reader.read().then((result) => ({ reader, result }));
pulling.set(reader, p);
return p;
}
// Initialise pulls
for (const reader of readers) {
pullFrom(reader);
}
return new ReadableStream<T>({
async pull(controller) {
while (pulling.size > 0) {
let reader: ReadableStreamDefaultReader<T>;
let result: ReadableStreamReadResult<T>;
try {
({ reader, result } = await Promise.race([
...pulling.values(),
]));
} catch (err) {
controller.error(err);
return;
}
pulling.delete(reader);
if (result.done) {
closedCount++;
if (closedCount === streams.length) {
controller.close();
return;
}
// Continue loop to race remaining readers
} else {
controller.enqueue(result.value);
pullFrom(reader);
return;
}
}
},
async cancel() {
await Promise.allSettled(readers.map((r) => r.cancel()));
},
});
}