Membaca dan menulis secara bertahap menjadi jauh lebih berharga ketika data bisa diubah di tengah perjalanan. Inilah ranah Transform: stream dua arah yang menerima potongan data, mengolahnya, lalu meneruskan hasilnya — mengubah huruf, mengompresi, menyaring baris, menyandi enkripsi. Tanpa Transform, kita harus menampung data di variabel perantara atau membuat dua stream terpisah yang saling menyalin secara manual. Chapter ini membahas cara membangun Transform sendiri, mulai dari pengubah sederhana sampai pengubah yang menyimpan state antar-potongan.

Transform: Mesin Pengubah di Tengah Pipa #

Transform adalah subclass dari Duplex dengan satu method wajib yang berbeda: transform(chunk, encoding, callback) — rincian kontraknya tercantum pada dokumentasi class stream.Transform . Setiap potongan data yang masuk melewati method ini; hasil pengolahan dikirim keluar melalui argumen callback. Contoh klasiknya adalah pengubah huruf kapital:

js
import { Readable, Transform, Writable, pipeline } from "node:stream";

const kapital = new Transform({
  transform(potongan, _encoding, callback) {
    callback(null, potongan.toString().toUpperCase());
  },
});

const hasil = [];
const penampung = new Writable({
  write(potongan, _encoding, callback) {
    hasil.push(potongan.toString());
    callback();
  },
});

pipeline(
  Readable.from(["belajar ", "node.js ", "itu ", "seru"]),
  kapital,
  penampung,
  (err) => {
    if (err) {
      console.error("gagal:", err.message);
      process.exitCode = 1;
      return;
    }
    console.log("hasil:", hasil.join(""));
  }
);

Output yang Dihasilkan:

text
hasil: BELAJAR NODE.JS ITU SERU

Mari kita telaah baris-baris kode di atas:

  1. Baris 3-7 (new Transform({...})): option transform menerima function pengolah. Nilai this di dalamnya adalah stream itu sendiri, sehingga method seperti this.push() juga tersedia.
  2. Baris 4 (transform(potongan, _encoding, callback)): potongan adalah data masuk; _encoding hanya relevan ketika potongan berupa string; callback dipanggil satu kali untuk setiap potongan.
  3. Baris 5 (callback(null, hasil)): argumen pertama untuk error (di sini null karena tidak ada masalah), argumen kedua adalah hasil pengolahan yang otomatis didorong ke sisi keluar stream. Bentuk callback(new Error(...)) akan menghentikan seluruh pipa.
  4. Baris 9-15 (penampung): Writable biasa yang mengumpulkan setiap potongan keluaran ke array hasil.
  5. Baris 17-29 (pipeline(...)): function pipeline merangkai tiga stream — sumber, pengubah, tujuan — dan memanggil callback penutup setelah aliran tuntas. Bentuk pemanggilan lengkapnya dengan seluruh variasinya dibedah pada dokumentasi stream.pipeline() ; kita bedah lebih dalam pada chapter berikutnya.

Menyimpan State Antar-Potongan #

Pengolah nyata sering butuh mengingat sesuatu dari potongan sebelumnya — nomor urut, jumlah baris, sisa kata yang belum lengkap. Karena this di dalam transform adalah stream itu sendiri, state cukup disimpan sebagai properti. Contoh berikut membuat pengubah penomor baris:

js
import { Readable, Transform } from "node:stream";

const penomorBaris = new Transform({
  objectMode: true,
  transform(baris, _encoding, callback) {
    this.nomor = (this.nomor ?? 0) + 1;
    callback(null, `${this.nomor}. ${baris}`);
  },
});

Readable.from([
  "Belajar Buffer",
  "Menulis dengan stream",
  "Memahami backpressure",
]).pipe(penomorBaris);

for await (const baris of penomorBaris) {
  console.log(baris);
}
console.log("selesai");

Output yang Dihasilkan:

text
1. Belajar Buffer
2. Menulis dengan stream
3. Memahami backpressure
selesai

Dua hal baru muncul pada contoh di atas. Pertama, baris 4 (objectMode: true): stream secara bawaan memindahkan byte atau string; objectMode mengizinkan potongan berupa nilai JavaScript apa pun. Readable.from() memang bermode object, jadi option ini membuat penomorBaris cocok menerimanya tanpa konversi. Kedua, baris 6 (this.nomor ?? 0): operator nullish mengambil nilai lama this.nomor atau 0 saat belum ada — nomor bertambah satu pada tiap potongan, menempel pada hasil sebagai awalan. Baris 11-15 menyambungkan sumber ke pengubah dengan pipe(), lalu baris 17 mengonsumsi sisi keluarnya dengan for await — sebuah Transform memang bisa diperlakukan sebagai Readable penuh.

Memperhatikan Batas Potongan #

Batas potongan stream ditentukan ketersediaan data, bukan oleh struktur teks. Pengolah yang berasumsi satu potongan sama dengan satu baris atau satu kata akan rusak pada data nyata — panduan menulis stream pengolah yang tangguh tersedia pada bagian API for stream implementers . Dua strategi mengatasinya:

  1. Sambungkan sendiri potongan-potongan: simpan sisa string yang belum berakhir baris sebagai state, lalu satukan dengan potongan berikutnya sebelum diproses. Inilah pola yang dipakai readline di balik layar.
  2. Pecah sendiri potongan besar: untuk sumber berpotongan besar, panggil this.push() beberapa kali di dalam satu transform — callback hanya perlu dipanggil sekali di akhir.

Transform stream dari core module memakai mekanisme yang persis sama dengan yang baru kita tulis: zlib.createGzip() mengompresi, crypto menyandi, dan sebagainya. Hal yang belum kita punya adalah cara merangkai banyak stream sekaligus dengan penanganan error yang layak — mulai dari sederhananya pipe() hingga andalnya pipeline(), semuanya dibahas pada Chapter pipe dan pipeline.