Signatur
Beschreibung
Der Konstruktor TransformStream() erzeugt ein Stream-Paar: Was in die Eigenschaft writable (ein WritableStream) geschrieben wird, läuft durch die Funktion transform() des übergebenen Transformer-Objekts und kommt anschließend an der Eigenschaft readable (ein ReadableStream) heraus. So lassen sich Umwandlungen wie Textkodierung, Zeilentrennung, Filterung, Komprimierung oder JSON-Parsing als wiederverwendbare Bausteine bauen.
Typischerweise setzt man einen TransformStream mit ReadableStream.prototype.pipeThrough() in eine Pipeline ein, zum Beispiel zwischen den Body einer fetch()-Antwort und den eigenen Code, der die Daten verarbeitet. Der Transformer kann drei optionale Rückruffunktionen haben:
start(controller): wird einmal sofort beim Erzeugen aufgerufen, z. B. um einen Zustand anzulegen.transform(chunk, controller): wird für jeden geschriebenen Chunk aufgerufen. Mitcontroller.enqueue()gibt man 0, 1 oder mehrere Ausgabe-Chunks weiter.flush(controller): wird aufgerufen, wenn die beschreibbare Seite geschlossen wird. Hier gibt man gepufferte Restdaten aus.
Alle drei Funktionen dürfen ein Promise zurückgeben. Die Pipeline wartet dann, bis es erfüllt ist, und so entsteht automatisch Gegendruck (Backpressure). Ohne Transformer entsteht ein Identitäts-Stream, der Chunks unverändert weiterreicht. Das ist nützlich, um eine beschreibbare Seite an einen Produzenten zu geben und die lesbare Seite z. B. als Response-Body zurückzugeben.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $transformer | object | {} (Identitäts-Transformation) | Objekt mit den optionalen Methoden start(controller), transform(chunk, controller), flush(controller) und (neu) cancel(reason). controller ist ein TransformStreamDefaultController mit enqueue(), error(), terminate() und desiredSize. Die Eigenschaften readableType und writableType sind reserviert und müssen undefined sein. |
| $writableStrategy | QueuingStrategy | { highWaterMark: 1 } | Warteschlangen-Strategie der beschreibbaren Seite: highWaterMark (Anzahl oder Größe) und optional size(chunk). Alternativ eine CountQueuingStrategy oder ByteLengthQueuingStrategy. |
| $readableStrategy | QueuingStrategy | { highWaterMark: 0 } | Warteschlangen-Strategie der lesbaren Seite. Der Standardwert 0 bedeutet, dass erst dann transformiert wird, wenn jemand liest. |
Rückgabewert
TransformStream-Objekt mit den Eigenschaften readable (ReadableStream) und writable (WritableStream).Beispiele
Einfacher Transformer: Text in Großbuchstaben
const upperCase = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
}
});
const source = ReadableStream.from(['hallo ', 'welt']);
const result = await new Response(
source.pipeThrough(upperCase)
).text();
console.log(result);
Zeilenweise Verarbeitung einer fetch-Antwort mit flush()
// Teilt Text-Chunks in einzelne Zeilen auf und puffert unvollständige Zeilen
const lineSplitter = () => {
let buffer = '';
return new TransformStream({
transform(chunk, controller) {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop(); // letzte, evtl. unvollständige Zeile behalten
for (const line of lines) controller.enqueue(line);
},
flush(controller) {
if (buffer) controller.enqueue(buffer);
}
});
};
const response = await fetch('/logs.ndjson');
const lines = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(lineSplitter());
for await (const line of lines) {
console.log(JSON.parse(line));
}
Identitäts-Stream als gestreamter Response-Body (z. B. im Service Worker)
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const encoder = new TextEncoder();
// Produzent schreibt asynchron, während der Body bereits ausgeliefert wird
(async () => {
for (let i = 1; i <= 3; i++) {
await writer.write(encoder.encode(`Teil ${i}\n`));
await new Promise((r) => setTimeout(r, 500));
}
await writer.close();
})();
const response = new Response(readable, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
console.log(await response.text());
Filtern und asynchrone Verarbeitung mit Backpressure
const onlyEven = new TransformStream({
async transform(n, controller) {
await new Promise((r) => setTimeout(r, 10)); // simulierte Arbeit
if (n % 2 === 0) controller.enqueue(n * 10);
}
}, { highWaterMark: 4 });
const out = [];
for await (const v of ReadableStream.from([1, 2, 3, 4, 5, 6]).pipeThrough(onlyEven)) {
out.push(v);
}
console.log(out);
// Wichtig · Fallstricke
- Deadlock-Falle: Wer bei einem Identitäts-Stream ohne parallelen Leser auf
writer.write()wartet, wartet unter Umständen ewig, weil die Warteschlange voll ist und nichts gelesen wird. Deshalb in Schreiber und Leser nebenläufig schreiben und lesen, wie im Response-Beispiel. - flush() nicht vergessen: Wer Chunks puffert (Zeilen, JSON, Blöcke), muss Restdaten in
flush()ausgeben. Sonst gehen sie am Stream-Ende verloren. - Fehler: Wirft
transform()eine Ausnahme oder wird sein Promise abgelehnt, wechseln beide Seiten in den Fehlerzustand.controller.terminate()schließt die lesbare Seite und setzt die beschreibbare Seite in den Fehlerzustand. - Chunk-Grenzen sind beliebig: Ein Multibyte-Zeichen oder eine Zeile kann auf zwei Chunks verteilt sein. Für Text daher
TextDecoderStreamvorschalten. - Kompatibilität: In Chrome/Edge 67+, Firefox 102+ und Safari 14.1+ verfügbar, in Node.js 18+ global (vorher über
stream/web).ReadableStream.from()und die asynchrone Iteration vonReadableStreamsind neuer und fehlen in älteren Safari-Versionen. Der Transformer-Callbackcancel()ist erst seit Kurzem umgesetzt. - Streams sind übertragbar (
postMessagemit Transfer-Liste). So kann eine Transformation in einem Worker laufen, sofern der Browser das unterstützt.