Antrean pada Bagian 1 sudah bisa menyimpan pekerjaan, tetapi belum ada yang mengerjakan. Pada bagian ini kita menghidupkan pelaksananya: worker — satu loop yang mengambil pekerjaan dari barisan, menjalankan handler yang sesuai, lalu mengulang sampai barisan kosong. Desain yang kita pilih tidak memakai setInterval yang bangun setiap beberapa milidetik untuk memeriksa antrean; worker kita bangun lewat kejadian: setiap pemasukan pekerjaan memicu loop bila loop sedang tidak berjalan. Sepanjang bagian ini kita juga memasang peristiwa PekerjaanSelesai dan PekerjaanGagal — pola EventEmitter yang sudah kita kenal pada Chapter Event-Driven Architecture.

Handler sebagai Unit Kerja #

Sebelum berbicara loop, kita perlu mendefinisikan apa yang dikerjakan. Setiap jenis pekerjaan dipetakan ke satu function async dalam registry HANDLER_BAWAAN:

js src/handler.js
export const HANDLER_BAWAAN = {
  'kirim-email': async (pekerjaan) => {
    const { penerima, subjek } = pekerjaan.data;
    if (typeof penerima !== 'string' || !penerima.includes('@')) {
      throw new Error(`Penerima email tidak valid: ${penerima}`);
    }
    if (typeof subjek !== 'string' || subjek.trim() === '') {
      throw new Error('Subjek email wajib diisi');
    }
    await Promise.resolve();
    await Promise.resolve();
    return `Email "${subjek}" terkirim ke ${penerima}`;
  },

Handler ini meniru pekerjaan nyata tanpa I/O sungguhan: dua await Promise.resolve() pada baris 12-13 berperan sebagai titik jeda asynchronous tempat operasi jaringan biasa berlangsung, sehingga perilaku antrean sama dengan memakai handler yang benar-benar menunggu. Validasi penerima dan subjek sengaja diletakkan di dalam handler, bukan pada validasi input Bagian 1 — penerima berformat email adalah syarat pemrosesan, bukan syarat pemasukan. Konsekuensinya terlihat pada jalur kegagalan di Bagian 3: pekerjaan seperti itu diterima, lalu gagal saat dikerjakan. Registry juga berisi jenis buat-laporan dengan bentuk yang sama; jenis baru cukup ditambahkan sebagai pasangan kunci-function baru tanpa menyentuh worker.

Loop yang Menguras Antrean #

Jantung worker ada pada dua function kecil. Yang pertama menjalankan satu pekerjaan; yang kedua mengulanginya sampai barisan kosong:

js src/pekerja.js
  async function prosesSatu(pekerjaan) {
    let hasil;
    try {
      hasil = await jalankanHandler(pekerjaan);
    } catch (err) {
      return tanganiKegagalan(pekerjaan, err);
    }
    bus.emit('PekerjaanSelesai', antrean.tandaiSelesai(pekerjaan.id, hasil));
  }

  async function kerjakan() {
    while (berjalan) {
      const pekerjaan = antrean.ambilBerikutnya();
      if (pekerjaan === null) break;
      await prosesSatu(pekerjaan);
    }
  }

Bedah baris-per-baris:

  1. Baris 29-37 (prosesSatu): try hanya membungkus pemanggilan handler. Bila handler melempar error, alur masuk ke tanganiKegagalan — function yang kita bedah tuntas pada Bagian 3. Baris bus.emit di baris 36 sengaja berada di luar try; error dari listener peristiwa bukan kegagalan pekerjaan, jadi tidak boleh tertangkap oleh catch yang sama.
  2. Baris 39-45 (kerjakan): loop while (berjalan) mengambil pekerjaan sampai barisan kosong (break saat null) atau sampai worker diberhentikan — variabel berjalan diperiksa sebelum setiap pengambilan, sehingga berhenti() tidak pernah memotong pekerjaan yang sedang berjalan; pekerjaan itu diselesaikan lebih dulu, lalu loop keluar dengan rapi.
  3. await pada baris 43: karena setiap pekerjaan ditunggu sampai tuntas, satu worker memproses pekerjaan satu per satu secara berurutan — perilaku yang diinginkan untuk pekerjaan seperti pengiriman email yang tidak boleh berebut urutan.

Sebelum handler dijalankan, jalankanHandler mencari function yang cocok di registry dan melempar error bila jenis tidak dikenal — jaring pengaman terakhir untuk pekerjaan yang masuk ke antrean tanpa melewati validasi pemroses.

Bangunkan, Mulai, dan Berhenti #

Loop tidak berjalan terus-menerus; ia bangkit hanya saat ada pekerjaan. Mekanisme bangunkannya sekecil ini:

js src/pekerja.js
  function bangunkan() {
    if (!berjalan || siklus !== null) return;
    siklus = kerjakan().finally(() => {
      siklus = null;
    });
    // Penolakan tidak dibiarkan tanpa penanganan; galat tetap terlihat
    // lewat await selesai() pada pemanggil.
    siklus.catch(() => {});
  }

Variabel siklus menyimpan promise loop yang sedang berjalan. Pemanggilan bangunkan saat loop masih aktif tidak berbuat apa-apa — cukup satu loop yang menguras antrean. Saat kerjakan selesai, finally mengosongkan siklus kembali. Method publik worker tinggal merangkai mekanisme ini:

js src/pekerja.js
    mulai() {
      berjalan = true;
      bangunkan();
    },

    berhenti() {
      berjalan = false;
    },

    bangunkan,

    async selesai() {
      while (siklus !== null || (berjalan && antrean.statistik().menunggu > 0)) {
        if (siklus !== null) {
          await siklus;
          continue;
        }
        bangunkan();
      }
    },

mulai mengibarkan bendera berjalan lalu membangunkan loop; berhenti hanya menurunkan bendera itu. Method selesai adalah kontrak paling penting: ia mengembalikan promise yang baru selesai setelah antrean benar-benar kering — menunggu siklus yang sedang berjalan, lalu memeriksa ulang apakah masih ada pekerjaan menunggu sambil worker aktif. Inilah pengganti setTimeout dalam test: alih-alih “tunggu 100 milidetik lalu berharap selesai”, kita menunggu keadaan akhirnya secara langsung.

Pemroses pada Bagian 1 memanggil pekerja.bangunkan() setiap kali pekerjaan masuk — nah, sekarang jelas gunanya: pekerjaan baru memicu loop bila worker menyala, dan tidak berbuat apa-apa bila belum. Method mulai, berhenti, selesai, dan on pemroses hanya meneruskan ke worker, sehingga pengguna API tidak perlu menyentuh worker secara langsung.

Peristiwa Selesai dan Gagal #

Dua baris bus.emit pada prosesSatu dan tanganiKegagalan memancarkan peristiwa setiap kali satu pekerjaan mencapai status akhir, dengan salinan pekerjaan sebagai muatannya. Nama peristiwa sengaja berbentuk fakta lampau — PekerjaanSelesai, PekerjaanGagal — mengikuti panduan penamaan dari Chapter Event-Driven Architecture.

Sesi berikut memperlihatkan seluruh siklus worker: dua pekerjaan dikuras pada siklus pertama, satu pekerjaan masuk saat worker idle dan memicu siklus kedua, lalu worker dihentikan sehingga pekerjaan berikutnya tetap menunggu:

bash
node src/demo-pekerja.mjs

Output yang Dihasilkan:

text
[selesai] #1 kirim-email -> Email "Kwitansi" terkirim ke [email protected]
[selesai] #2 buat-laporan -> Laporan "Laporan mingguan" selesai dibuat
setelah siklus pertama: {"menunggu":0,"diproses":0,"selesai":2,"gagal":0,"total":2}
[selesai] #3 kirim-email -> Email "Pengingat" terkirim ke [email protected]
setelah siklus kedua : {"menunggu":0,"diproses":0,"selesai":3,"gagal":0,"total":3}
pekerjaan terakhir   : {"id":4,"jenis":"buat-laporan","data":{"judul":"Laporan tertunda"},"status":"menunggu","percobaan":0}

Perhatikan baris setelah siklus kedua lalu pekerjaan terakhir: setelah berhenti(), pekerjaan keempat tetap menunggu meski await selesai() sudah berlalu — kontrak selesai hanya menunggu pekerjaan yang bisa dikerjakan, dan worker yang berhenti memang tidak bisa. Jalur sukses kini utuh; yang belum kita tangani adalah jalur gagal: apa yang terjadi ketika handler melempar error. Bagian 3 menjawabnya dengan percobaan ulang.