Signatur
Beschreibung
Die Schnittstelle TransformStream gehört zur Streams API und steht für ein Glied in einer Pipe-Kette, das Daten umwandelt. Sie stellt zwei Eigenschaften bereit: writable (ein WritableStream), in das Datenblöcke (Chunks) geschrieben werden, und readable (ein ReadableStream), über das die umgewandelten Chunks wieder herauskommen.
Welche Umwandlung passiert, legt ein Transformer-Objekt fest, das dem Konstruktor übergeben wird. Seine Methode transform(chunk, controller) wird für jeden Chunk aufgerufen. Mit controller.enqueue() gibt sie null, einen oder mehrere Ausgabe-Chunks weiter. flush(controller) läuft am Ende, bevor die lesbare Seite geschlossen wird – praktisch für gepufferte Restdaten.
Typische Einsatzgebiete sind Text-Dekodierung, Zeilen-Parser, Komprimierung, Filter und Zähler, die mit readable.pipeThrough(transformStream) in eine Kette eingehängt werden. Weil Daten Stück für Stück verarbeitet werden, bleibt der Speicherbedarf auch bei großen Antworten (z. B. aus fetch()) gering. Über die Warteschlangen-Strategien wird automatisch Gegendruck (Backpressure) erzeugt.
Fertige Unterklassen bzw. verwandte Transform-Streams gibt es schon: TextDecoderStream, TextEncoderStream, CompressionStream und DecompressionStream.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $transformer | object | {} (Identitäts-Transformation) | Objekt mit optionalen Methoden: start(controller) (wird einmal beim Erzeugen aufgerufen), transform(chunk, controller) (für jeden Chunk; darf ein Promise zurückgeben), flush(controller) (am Ende), cancel(reason) (wenn abgebrochen wird). controller ist ein TransformStreamDefaultController mit enqueue(), error(), terminate() und desiredSize. Ohne transform werden Chunks unverändert weitergereicht. |
| $writableStrategy | QueuingStrategy | { highWaterMark: 1 } | Warteschlangen-Strategie der beschreibbaren Seite, mit highWaterMark und optional size(chunk), z. B. eine CountQueuingStrategy oder ByteLengthQueuingStrategy. |
| $readableStrategy | QueuingStrategy | { highWaterMark: 0 } | Warteschlangen-Strategie der lesbaren Seite. |
Beispiele
Einfacher Transform-Stream: Text in Großbuchstaben
const upperCase = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
}
});
const source = new ReadableStream({
start(controller) {
controller.enqueue('hallo ');
controller.enqueue('welt');
controller.close();
}
});
const result = source.pipeThrough(upperCase);
for await (const chunk of result) {
console.log(chunk);
}
Fetch-Antwort zeilenweise verarbeiten (mit flush)
const lineSplitter = () => {
let buffer = '';
return new TransformStream({
transform(chunk, controller) {
buffer += chunk;
const lines = buffer.split('\n');
buffer = lines.pop(); // unvollständige Zeile aufheben
for (const line of lines) controller.enqueue(line);
},
flush(controller) {
if (buffer) controller.enqueue(buffer); // Rest ausgeben
}
});
};
const response = await fetch('/logs/server.log');
const lines = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(lineSplitter());
let count = 0;
for await (const line of lines) {
if (line.includes('ERROR')) count++;
}
console.log(`Fehlerzeilen: ${count}`);
writable und readable direkt verwenden
const { readable, writable } = new TransformStream({
transform(obj, controller) {
controller.enqueue(JSON.stringify(obj) + '\n');
}
});
// Schreiben und Lesen parallel, sonst blockiert der Gegendruck
const writeTask = (async () => {
const writer = writable.getWriter();
await writer.write({ id: 1, name: 'Anna' });
await writer.write({ id: 2, name: 'Ben' });
await writer.close();
})();
const text = await new Response(readable).text();
await writeTask;
console.log(text);
Asynchrone Transformation und Komprimierung verketten
const addTimestamp = new TransformStream({
async transform(chunk, controller) {
await new Promise((r) => setTimeout(r, 10)); // z. B. asynchrone Prüfung
controller.enqueue(`[${new Date().toISOString()}] ${chunk}\n`);
}
});
const source = ReadableStream.from(['Start', 'Stopp']);
const compressed = source
.pipeThrough(addTimestamp)
.pipeThrough(new TextEncoderStream())
.pipeThrough(new CompressionStream('gzip'));
const blob = await new Response(compressed).blob();
console.log(`Komprimiert: ${blob.size} Bytes`);
// Wichtig · Fallstricke
- Gegendruck/Deadlock: Wer mit
writer.write()schreibt und jedes Promise abwartet, ohne dass gleichzeitig jemandreadableliest, bleibt hängen, denn der StandardwerthighWaterMark: 0auf der lesbaren Seite lässt nichts in die Warteschlange. Deshalb Schreiben und Lesen parallel laufen lassen oderpipeThrough()verwenden. - Fehler: Wirft
transform()eine Ausnahme oder gibt ein abgelehntes Promise zurück, gehen beide Seiten in den Fehlerzustand. Mitcontroller.error()lässt sich das gezielt auslösen,controller.terminate()schließt die lesbare Seite und bricht die beschreibbare ab. - Chunk-Grenzen: Netzwerk-Chunks halten sich nicht an Zeilen- oder Zeichengrenzen. Rest-Daten puffern und in
flush()ausgeben (siehe Beispiel 2). - Übertragbar:
TransformStreamist transferable und kann perpostMessage()an einen Worker übergeben werden, damit die Umwandlung dort läuft. - Kompatibilität: Unterstützt in Chrome 67+, Edge 79+, Firefox 102+, Safari 14.1+ sowie Node.js 18+ (global; ab 16.5 über
stream/web). Diecancel()-Methode des Transformers ist neuer und fehlt in älteren Engines. Asynchrone Iteration mitfor awaitüber einenReadableStreamundReadableStream.from()werden in Safari erst seit Kurzem unterstützt.