Signatur
Beschreibung
Mit dem Konstruktor new ReadableStream() baust du eine eigene lesbare Datenquelle im Sinne der Streams API. Du übergibst ein Objekt mit Handlern (die underlying source). Diese legen fest, wie Datenstücke (chunks) in die interne Warteschlange des Streams kommen. Wer den Stream nutzt, liest die Daten dann über einen Reader, über for await...of oder über pipeTo()/pipeThrough() aus.
Die wichtigsten Handler sind:
start(controller)wird sofort beim Erzeugen aufgerufen. Er eignet sich für die Einrichtung oder für Push-Quellen wie WebSockets oder Ereignisse, die von sich aus Daten liefern.pull(controller)wird immer dann aufgerufen, wenn in der Warteschlange Platz ist. Er eignet sich für Pull-Quellen, also Daten, die man auf Anfrage erzeugt oder lädt. So bekommst du automatisch Gegendruck (Backpressure): Die Quelle liefert nur so schnell, wie der Leser verarbeitet.cancel(reason)wird aufgerufen, wenn der Leser den Stream abbricht. Dort gibst du Ressourcen frei.
Mit type: "bytes" entsteht ein Byte-Stream mit einem ReadableByteStreamController. Er erlaubt BYOB-Reader (bring your own buffer), die ohne zusätzliches Kopieren lesen. Ohne type nimmt der Stream beliebige JavaScript-Werte als Chunks an.
Typische Einsatzfälle sind das Streamen einer fetch-Antwort mit selbst erzeugtem Inhalt, zum Beispiel über new Response(stream), das Einpacken von Ereignisquellen in einen Stream und Pipelines mit TransformStream, etwa für Kompression oder Textdekodierung.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $underlyingSource | object | {} | Objekt mit optionalen Einträgen: start(controller), pull(controller), cancel(reason), type (nur "bytes" erlaubt) und autoAllocateChunkSize (positive Ganzzahl, nur bei Byte-Streams). start und pull dürfen ein Promise zurückgeben. Solange es nicht erfüllt ist, ruft der Stream pull nicht erneut auf. |
| $queuingStrategy | object | CountQueuingStrategy | ByteLengthQueuingStrategy | { highWaterMark: 1 } (bei Byte-Streams: 0) | Legt die Warteschlangen-Strategie fest: highWaterMark ist die Füllmenge, ab der kein pull mehr ausgelöst wird. size(chunk) berechnet die Größe eines Chunks und ist bei Byte-Streams nicht erlaubt. |
Rückgabewert
ReadableStream-Objekt, das die Daten aus der angegebenen Quelle liefert.Beispiele
Einfacher Stream mit start() und Reader
const stream = new ReadableStream({
start(controller) {
controller.enqueue('Hallo');
controller.enqueue('Welt');
controller.close(); // keine weiteren Daten
}
});
const reader = stream.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(value);
}
Pull-Quelle mit Gegendruck und for await...of
// Erzeugt Zahlen nur, wenn der Leser sie auch anfordert
let i = 0;
const zahlen = new ReadableStream({
pull(controller) {
if (i >= 3) {
controller.close();
return;
}
controller.enqueue(i++);
},
cancel(reason) {
console.log('Abgebrochen:', reason);
}
}, { highWaterMark: 2 });
// ReadableStream ist async-iterierbar (Node.js, Firefox, Chrome 124+)
for await (const n of zahlen) {
console.log('Zahl', n);
}
Selbst erzeugten Text als fetch-Response streamen
const encoder = new TextEncoder();
const zeilen = ['Zeile 1\n', 'Zeile 2\n', 'Zeile 3\n'];
const stream = new ReadableStream({
async pull(controller) {
const zeile = zeilen.shift();
if (zeile === undefined) return controller.close();
await new Promise(r => setTimeout(r, 100)); // simulierte Verzögerung
controller.enqueue(encoder.encode(zeile));
}
});
// z. B. in einem Service Worker: event.respondWith(response)
const response = new Response(stream, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
console.log(await response.text());
Ereignisse (Push-Quelle) in einen Stream einpacken
const klickStream = (element) => {
let handler;
return new ReadableStream({
start(controller) {
handler = (e) => controller.enqueue({ x: e.clientX, y: e.clientY });
element.addEventListener('click', handler);
},
cancel() {
// Aufräumen, wenn der Leser abbricht
element.removeEventListener('click', handler);
}
});
};
const reader = klickStream(document.body).getReader();
const { value } = await reader.read();
console.log('Erster Klick bei', value);
await reader.cancel('genug');
// Wichtig · Fallstricke
- Gegendruck bei Push-Quellen: Wenn du in
startungebremstenqueue()aufrufst, wird der Gegendruck nicht beachtet und die Warteschlange kann ohne Grenze wachsen. Prüfe danncontroller.desiredSize(≤ 0 bedeutet: Quelle pausieren) oder verwende besserpull. - Gesperrter Stream: Nach
getReader()ist der Stream gesperrt (stream.locked === true). Ein zweiter Reader oderpipeTo()führt dann zu einemTypeError. Gib die Sperre mitreader.releaseLock()frei. Wenn mehrere Leser dieselben Daten brauchen, nutzestream.tee(). - Fehler: Wirft
startoderpulleine Ausnahme oder wird ihr Promise abgelehnt, geht der Stream in den Fehlerzustand über. Du kannst auch selbstcontroller.error(e)aufrufen. Nachclose()führt ein weiteresenqueue()zu einemTypeError. - Byte-Streams: Mit
type: "bytes"darfst du nurArrayBufferViews (z. B.Uint8Array) einreihen. Außerdem darfqueuingStrategy.sizenicht gesetzt sein, sonst gibt es einenRangeError. Byte-Streams unterstützt Safari erst seit Version 18.4 (bitte gegenprüfen), deshalb solltest du vor der Nutzung prüfen, ob die Funktion vorhanden ist. - Kompatibilität: Der Konstruktor ist in allen modernen Browsern verfügbar, in Node.js global ab Version 18 (vorher über
stream/web). Async-Iteration (for await...of) über ReadableStream gibt es in Chrome/Edge ab Version 124, in Firefox und Node.js schon länger. Safari unterstützt sie noch nicht. Als Ausweichlösung verwendest du dort einen Reader. - Einen Stream als Anfrage-Body zu senden (
fetch(url, { body: stream, duplex: 'half' })) funktioniert bisher nur in Chromium-Browsern und nur über HTTP/2 oder neuer.