/** * 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(streams: ReadableStream[]): ReadableStream { 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, Promise<{ reader: ReadableStreamDefaultReader; result: ReadableStreamReadResult; }> >(); function pullFrom(reader: ReadableStreamDefaultReader): Promise<{ reader: ReadableStreamDefaultReader; result: ReadableStreamReadResult; }> { 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({ async pull(controller) { while (pulling.size > 0) { let reader: ReadableStreamDefaultReader; let result: ReadableStreamReadResult; 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())); }, }); }