Initial Commit
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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<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 sources = [...streams];
|
||||
let index = 0;
|
||||
let currentReader: ReadableStreamDefaultReader<T> | undefined;
|
||||
let cancelled = false;
|
||||
|
||||
function acquireReader(): ReadableStreamDefaultReader<T> | undefined {
|
||||
if (currentReader) return currentReader;
|
||||
if (index >= sources.length) return undefined;
|
||||
currentReader = sources[index]!.getReader();
|
||||
return currentReader;
|
||||
}
|
||||
|
||||
return new ReadableStream<T>({
|
||||
async pull(controller) {
|
||||
while (!cancelled) {
|
||||
const reader = acquireReader();
|
||||
if (!reader) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
let result: ReadableStreamReadResult<T>;
|
||||
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.
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user