import { StreamPipeFailedError } from "../../errors/stream-error.js"; /** * Concatenates multiple ReadableStreams into one, sequentially. * * Each input stream is drained to completion before the next stream is pulled * from. Output ordering is therefore deterministic and identical to the input * order — unlike {@link merge}, which is race-based. * * Errors from any input stream halt the concatenation immediately and are * propagated wrapped in a {@link StreamPipeFailedError}; subsequent streams * are never read. Output cancellation cancels the currently-reading source * and releases any unread sources without pulling from them. */ export function concat(streams: ReadableStream[]): ReadableStream { if (streams.length === 0) { return new ReadableStream({ start(c) { c.close(); }, }); } if (streams.length === 1) { return streams[0]!; } const sources = [...streams]; let index = 0; let currentReader: ReadableStreamDefaultReader | undefined; let cancelled = false; function acquireReader(): ReadableStreamDefaultReader | undefined { if (currentReader) return currentReader; if (index >= sources.length) return undefined; currentReader = sources[index]!.getReader(); return currentReader; } return new ReadableStream({ async pull(controller) { while (!cancelled) { const reader = acquireReader(); if (!reader) { controller.close(); return; } let result: ReadableStreamReadResult; try { result = await reader.read(); } catch (cause) { controller.error( new StreamPipeFailedError({ cause, message: "concat: input stream errored", }) ); // Release the failed reader; do not touch unread sources. try { reader.releaseLock(); } catch { // ignore — reader may already be released } currentReader = undefined; return; } if (result.done) { try { reader.releaseLock(); } catch { // ignore } currentReader = undefined; index++; // Loop to acquire next reader (or close if exhausted). continue; } controller.enqueue(result.value); return; } }, async cancel(reason) { cancelled = true; const reader = currentReader; currentReader = undefined; try { await reader?.cancel(reason); } catch { // best-effort cancellation } // Subsequent un-acquired sources are intentionally NOT pulled or // cancelled — concat consumes them lazily, so unread sources were // never attached and remain owned by the caller. }, }); }