Start · Sprachen · JavaScript · Referenz · TransformStream

TransformStream

Schnittstelle

Ein <code>TransformStream</code> ist ein Paar aus beschreibbarem und lesbarem Stream: Die geschriebenen Daten werden umgewandelt und lassen sich danach auf der lesbaren Seite abholen.

seit JavaScript Baseline: alle modernen Browser (seit 2022), Node. Kategorie: webapi

Signatur

interface TransformStream

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);
}
HALLO WELT

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}`);
Fehlerzeilen: 3

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);
{"id":1,"name":"Anna"} {"id":2,"name":"Ben"}

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`);
Komprimiert: 78 Bytes

// Wichtig · Fallstricke

  • Gegendruck/Deadlock: Wer mit writer.write() schreibt und jedes Promise abwartet, ohne dass gleichzeitig jemand readable liest, bleibt hängen, denn der Standardwert highWaterMark: 0 auf der lesbaren Seite lässt nichts in die Warteschlange. Deshalb Schreiben und Lesen parallel laufen lassen oder pipeThrough() verwenden.
  • Fehler: Wirft transform() eine Ausnahme oder gibt ein abgelehntes Promise zurück, gehen beide Seiten in den Fehlerzustand. Mit controller.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: TransformStream ist transferable und kann per postMessage() 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). Die cancel()-Methode des Transformers ist neuer und fehlt in älteren Engines. Asynchrone Iteration mit for await über einen ReadableStream und ReadableStream.from() werden in Safari erst seit Kurzem unterstützt.