78 lines
2.3 KiB
TypeScript
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()));
|
|
},
|
|
});
|
|
}
|