Sisi penerima stream bekerja dengan logika yang berkebalikan dari Readable: data masuk dari kode kita, lalu dikirim tujuannya secara bertahap. Writable stream muncul di banyak tempat — fs.createWriteStream() untuk berkas, process.stdout untuk terminal, body respons HTTP untuk klien yang menunggu — dan perilakunya dirinci pada dokumentasi class stream.Writable . Menulis lewat stream berarti kita tidak perlu merakit seluruh isi di memori sebelum mengirimnya keluar. Chapter ini membahas siklus hidup penulisan yang benar: write(), end(), event finish, hingga penanganan buffer internal yang penuh lewat event drain.

Siklus hidup Writable paling sederhana terdiri atas tiga langkah: tulis data dengan write(), tandai selesai dengan end(), lalu tunggu event finish. Pembuka stream berkas dilakukan oleh fs.createWriteStream() — seluruh opsinya dirinci pada dokumentasi fs. Contoh berikut menulis tiga baris ke berkas keluaran.txt, membaca ulang isinya sebagai bukti, lalu menghapus berkas tersebut agar folder tetap bersih:

js
import { createWriteStream } from "node:fs";
import { readFile, rm } from "node:fs/promises";

const NAMA = "keluaran.txt";
const tulis = createWriteStream(NAMA);

tulis.write("baris pertama\n");
tulis.write("baris kedua\n");
tulis.end("baris penutup\n");

tulis.on("finish", async () => {
  const isi = await readFile(NAMA, "utf8");
  console.log("isi berkas setelah finish:");
  process.stdout.write(isi);
  await rm(NAMA, { force: true });
  console.log("berkas sementara dihapus");
});

Output yang Dihasilkan:

text
isi berkas setelah finish:
baris pertama
baris kedua
baris penutup
berkas sementara dihapus

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (createWriteStream(NAMA)): berkas dibuka untuk ditulis. Option flags bawaannya "w", sehingga berkas lama dengan nama sama otomatis ditimpa dari awal — skrip ini aman dijalankan berulang kali.
  2. Baris 7-8 (tulis.write(...)): setiap panggilan write() menyerahkan data ke buffer internal stream. Method ini kembali seketika; penulisan sesungguhnya ke disk terjadi secara asynchronous di belakang layar.
  3. Baris 9 (tulis.end(...)): argumen terakhir sekaligus adalah data penutup. Setelah end() dipanggil, pemanggilan write() berikutnya memicu error — stream sudah menyatakan diri selesai.
  4. Baris 11 (tulis.on("finish", ...)): event finish terpicu hanya setelah seluruh data benar-benar tergelak ke tujuan. Di sinilah tempat yang tepat untuk langkah lanjutan seperti membaca ulang hasil, mengirim respons, atau membersihkan berkas sementara.

Buffer Internal dan Event drain #

write() tidak menunggu data sampai ke disk. Data menumpuk dahulu di buffer internal stream, dan method ini mengembalikan nilai boolean: true bila buffer masih lega, false bila buffer telah mencapai batas highWaterMark. Nilai false adalah sinyal agar kita menahan diri menulis lagi sampai event drain tiba:

js
import { createWriteStream } from "node:fs";
import { readFile, rm } from "node:fs/promises";

const NAMA = "catatan-drain.txt";
const tulis = createWriteStream(NAMA, { highWaterMark: 16 });

for (let i = 1; i <= 5; i++) {
  const baris = `catatan ${i}\n`;
  const masihCukup = tulis.write(baris);
  console.log(`write baris ${i}:`, masihCukup);
  if (!masihCukup) {
    await new Promise((resolve) => tulis.once("drain", resolve));
    console.log(`drain setelah baris ${i}`);
  }
}

tulis.end();
await new Promise((resolve) => tulis.once("finish", resolve));

const isi = await readFile(NAMA, "utf8");
console.log("panjang isi:", isi.length);
await rm(NAMA, { force: true });

Output yang Dihasilkan:

text
write baris 1: true
write baris 2: false
drain setelah baris 2
write baris 3: true
write baris 4: false
drain setelah baris 4
write baris 5: true
panjang isi: 50

Mari kita telaah bagian inti contoh di atas:

  1. Baris 5 (highWaterMark: 16): batas buffer internal diturunkan menjadi 16 byte agar perilaku drain cepat terlihat. Nilai bawaannya 16 kilobyte untuk stream berbasis byte.
  2. Baris 7-10 (tulis.write(baris)): baris pertama berukuran 10 byte — buffer berisi 10 dari 16 byte, masih lega, hasilnya true. Baris kedua membuat buffer mencapai 20 byte, melewati batas, sehingga hasilnya false.
  3. Baris 11-13 (await new Promise(...)): penulisan dijeda sampai event drain terpicu, yang menandakan buffer internal telah kosong dan tujuan siap menerima lagi. Setelah drain, siklus berulang — baris 3 mengembalikan true lagi karena buffer mulai dari kosong.
  4. Baris 18 (isi.length): seluruh 50 byte (lima baris masing-masing 10 byte) utuh tercatat — mekanisme jeda sama sekali tidak membuang data.

Mengabaikan nilai balik false tidak langsung menjadi error; data tetap masuk ke buffer. Bahayanya muncul pada data besar atau tanpa henti: buffer menumpuk terus di memori justru pada situasi yang ingin dihindari dengan stream. Disiplin menunggu drain inilah yang disebut backpressure, dengan pembahasan penuh di chapter terakhir Section ini. Bagian buffering pada dokumentasi stream merangkum perilaku buffer internal kedua jenis stream ini.

Memilih antara writeFile dan createWriteStream #

Kapan sebaiknya kita memakai stream untuk menulis? Panduan praktisnya sederhana:

SituasiPilihan tepat
Data kecil, sudah utuh di memori (konfigurasi, laporan singkat)writeFile
Data besar yang dirakit bertahap (laporan agregat, ekspor basis data)createWriteStream
Hasil harus dikirim seketika ke jaringan saat tersediaWritable HTTP response
Data berasal dari stream lainsambungkan dengan pipe/pipeline

Menulis dan membaca kini sama-sama kita kuasai. Sering kali keduanya perlu disambung dengan pengolah di tengah — mengubah huruf, mengompresi, menyaring baris. Pekerjaan tengah itulah ranah Transform, pokok bahasan Chapter Transform Stream.