Setiap sistem pemrosesan pekerjaan dimulai dari satu struktur data sederhana: daftar pekerjaan yang menunggu, diambil sesuai urutan kedatangan. Pada bagian ini kita membangun fondasi tersebut — antrean FIFO di memori lengkap dengan perpindahan statusnya — lalu memakainya dari luar lewat API HTTP. Satu hal penting sebelum mulai: pada tahap ini belum ada worker, jadi pekerjaan yang masuk akan tetap berstatus menunggu selamanya. Itu bukan kekurangan, melainkan desain — antrean dan pelaksananya adalah dua hal terpisah, dan memisahkannya membuat keduanya bisa diuji sendiri-sendiri.
Pekerjaan dan Statusnya #
Satu pekerjaan adalah object { id, jenis, data, status, percobaan } ditambah field hasil atau pesan yang muncul di akhir hidupnya. Nilai status bergerak mengikuti satu jalur:
| Status | Arti | Dipindahkan oleh |
|---|---|---|
menunggu | ada di barisan, menunggu giliran | tambah dan jadwalkanUlang |
diproses | sedang dijalankan handler | ambilBerikutnya |
selesai | handler berhasil, hasil terisi | tandaiSelesai |
gagal | percobaan habis, pesan terisi | tandaiGagal |
Dua struktur di dalam antrean menopang jalur itu: pekerjaan, sebuah Map
yang menyimpan seluruh data pekerjaan agar bisa dicari lewat id, dan barisan, array id yang mencatat urutan giliran. Pemisahan ini penting: daftar dan cari membaca dari Map tanpa peduli urutan, sementara ambilBerikutnya hanya melihat barisan.
Antrean FIFO dalam Satu Pabrik #
Fungsi buatAntrean membungkus kedua struktur tersebut dan hanya mengembalikan method — tanpa class, tanpa state yang bisa diubah dari luar:
export function buatAntrean() {
let idBerikutnya = 1;
const pekerjaan = new Map();
const barisan = [];
function salin(job) {
return { ...job };
}
return {
tambah(jenis, data) {
const job = { id: idBerikutnya, jenis, data, status: 'menunggu', percobaan: 0 };
idBerikutnya += 1;
pekerjaan.set(job.id, job);
barisan.push(job.id);
return salin(job);
},
ambilBerikutnya() {
const id = barisan.shift();
if (id === undefined) return null;
const job = pekerjaan.get(id);
job.status = 'diproses';
job.percobaan += 1;
return salin(job);
},Mari kita bedah baris-barisnya:
- Baris 6 (
idBerikutnya): id dihasilkan dari penghitung yang terus maju, bukan dari panjang barisan — pekerjaan yang dihapus atau diulang tidak membuat id terpakai ulang. - Baris 10-12 (
salin): setiap method mengembalikan salinan, bukan object internal. Pemanggil boleh mengubah salinannya tanpa merusak keadaan antrean; test pada Bagian 4 memastikan hal ini. - Baris 15-21 (
tambah): pekerjaan baru lahir denganstatus: 'menunggu'danpercobaan: 0, disimpan keMap, lalu id-nya didorong ke ujungbarisan— pekerjaan terakhir menunggu paling akhir. - Baris 23-30 (
ambilBerikutnya):shiftmengambil id paling depan; bila barisan kosong, nilai baliknyanull. Pekerjaan yang diambil berubah menjadidiprosesdanpercobaanbertambah satu — percobaan dihitung sejak pengambilan, sehingga pekerjaan yang sedang dikerjakan sudah tercatat sebagai percobaan pertamanya.
Menandai Hasil dan Penjadwalan Ulang #
Tiga method lainnya memindahkan pekerjaan ke status akhir atau mengembalikannya ke barisan:
tandaiSelesai(id, hasil) {
const job = pekerjaan.get(id);
if (job === undefined) return undefined;
job.status = 'selesai';
job.hasil = hasil;
return salin(job);
},
jadwalkanUlang(id) {
const job = pekerjaan.get(id);
if (job === undefined) return undefined;
job.status = 'menunggu';
barisan.push(id);
return salin(job);
},
tandaiGagal(id, pesan) {
const job = pekerjaan.get(id);
if (job === undefined) return undefined;
job.status = 'gagal';
job.pesan = pesan;
return salin(job);
},Perhatikan baris 43-44: jadwalkanUlang mendorong id ke ujung barisan, bukan ke depan. Pekerjaan yang gagal harus antre ulang di belakang pekerjaan lain yang sudah menunggu — kalau didorong ke depan, satu pekerjaan bermasalah akan mengunci seluruh antrean dengan percobaannya sendiri. Method daftar, cari, dan statistik melengkapi antrean dengan pembacaan; seluruh mekanisme ini bisa dilihat langsung tanpa HTTP:
node src/demo-antrean.mjsOutput yang Dihasilkan:
statistik awal : {"menunggu":3,"diproses":0,"selesai":0,"gagal":0,"total":3}
diambil pertama : {"id":1,"jenis":"kirim-email","data":{"penerima":"[email protected]","subjek":"Kwitansi"},"status":"diproses","percobaan":1}
setelah pengambilan: {"menunggu":2,"diproses":1,"selesai":0,"gagal":0,"total":3}
tandai selesai #1 : {"id":1,"jenis":"kirim-email","data":{"penerima":"[email protected]","subjek":"Kwitansi"},"status":"selesai","percobaan":1,"hasil":"Email terkirim"}
diambil kedua : {"id":2,"jenis":"buat-laporan","data":{"judul":"Laporan bulanan"},"status":"diproses","percobaan":1}
jadwalkan ulang #2 : {"id":2,"jenis":"buat-laporan","data":{"judul":"Laporan bulanan"},"status":"menunggu","percobaan":1}
diambil ketiga : {"id":3,"jenis":"kirim-email","data":{"penerima":"siti","subjek":"Undangan"},"status":"diproses","percobaan":1}
tandai gagal #3 : {"id":3,"jenis":"kirim-email","data":{"penerima":"siti","subjek":"Undangan"},"status":"gagal","percobaan":1,"pesan":"Penerima email tidak valid: siti"}
#2 kembali terambil: {"id":2,"jenis":"buat-laporan","data":{"judul":"Laporan bulanan"},"status":"diproses","percobaan":2}
tandai selesai #2 : {"id":2,"jenis":"buat-laporan","data":{"judul":"Laporan bulanan"},"status":"selesai","percobaan":2,"hasil":"Laporan siap"}
barisan kosong : null
statistik akhir : {"menunggu":0,"diproses":0,"selesai":2,"gagal":1,"total":3}Bacaan output: tiga pekerjaan masuk lalu menunggu: 3; pengambilan pertama mengubah #1 menjadi diproses dengan percobaan: 1; setelah #2 dijadwalkan ulang, giliran berikutnya jatuh ke #3 — bukti penjadwalan ulang menempatkan #2 di ujung barisan; akhirnya #2 terambil kembali dengan percobaan: 2.
Pabrik Pemroses dan Server HTTP #
Antrean tidak berbicara HTTP, dan server tidak boleh tahu detail penyimpanan. Perantara keduanya adalah buatPemroses — pabrik yang merangkai antrean, handler, dan worker (yang kita bangun pada Bagian 2), lalu memaparkan method yang dibutuhkan server:
export function buatPemroses(opsi = {}) {
const antrean = opsi.antrean ?? buatAntrean();
const handler = opsi.handler ?? HANDLER_BAWAAN;
const maksimalPercobaan = opsi.maksimalPercobaan ?? 3;
const pekerja = buatPekerja({ antrean, handler, maksimalPercobaan });
return {
tambahPekerjaan(jenis, data) {
const galat = validasiPekerjaan(jenis, data, handler);
if (galat.length > 0) throw new GalatValidasi(galat);
const pekerjaanBaru = antrean.tambah(jenis, data);
pekerja.bangunkan();
return pekerjaanBaru;
},
daftarPekerjaan() {
return antrean.daftar();
},
cariPekerjaan(id) {
return antrean.cari(id);
},
statistik() {
return antrean.statistik();
},Dua keputusan penting berada di potongan ini. Pertama, baris 36-38: input divalidasi sebelum masuk antrean — jenis yang tidak terdaftar pada handler ditolak lewat GalatValidasi yang membawa daftar galat per field, sehingga server cukup menangkap satu jenis error untuk menjawab 422. Kedua, baris 39: setelah pekerjaan masuk, pekerja.bangunkan() memberi tahu worker — panggilan ini tidak berbuat apa pun selama worker belum dinyalakan, dan justru itulah alasan pekerjaan bisa tetap menunggu. Server-nya sendiri tipis:
export function buatServer(pemroses) {
return http.createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://${HOST}`);
const { aksi, cocok, pathDikenal } = cocokRute(req.method, url.pathname);
if (!aksi) {
if (pathDikenal) {
return kirimError(res, 405, 'METODE_TIDAK_DIIZINKAN',
`Metode ${req.method} tidak didukung pada endpoint ini`);
}
return kirimError(res, 404, 'ENDPOINT_TIDAK_ADA', 'Endpoint tidak ditemukan');
}
await aksi({ req, res, url, pemroses, cocok });
} catch (err) {
if (err instanceof GalatValidasi) {
return kirimError(res, 422, 'VALIDASI_GAGAL', err.message, err.detail);
}
if (err instanceof SyntaxError) {
return kirimError(res, 400, 'JSON_TIDAK_VALID', 'Body permintaan bukan JSON valid');
}
console.error(`[kesalahan internal] ${err.message}`);
if (res.headersSent) return res.end();
kirimError(res, 500, 'KESALAHAN_INTERNAL', 'Terjadi kesalahan tak terduga di sisi server');
}
});
}Handler menerima satu object konteks { req, res, url, pemroses, cocok } lewat dispatch di baris 95 — pola yang sama dengan Proyek REST API Manajemen Tugas — sehingga penambahan endpoint tidak mengubah signature. Tiga penanganan error di baris 96-105 memetakan error ke kode HTTP: GalatValidasi menjadi 422, SyntaxError dari JSON.parse menjadi 400, dan sisanya dicatat ke log server lalu dijawab 500.
node src/demo-api.mjsOutput yang Dihasilkan:
POST /pekerjaan -> 201 (lokasi: /pekerjaan/1) {"id":1,"jenis":"kirim-email","data":{"penerima":"[email protected]","subjek":"Kwitansi"},"status":"menunggu","percobaan":0}
POST /pekerjaan -> 201 {"id":2,"jenis":"buat-laporan","data":{"judul":"Laporan penjualan"},"status":"menunggu","percobaan":0}
POST /pekerjaan (jenis asing) -> 422 {"error":{"kode":"VALIDASI_GAGAL","pesan":"Data pekerjaan tidak lolos validasi","detail":[{"field":"jenis","pesan":"jenis tidak dikenal: kirim-faks"}]}}
POST /pekerjaan (bukan JSON) -> 400 {"error":{"kode":"JSON_TIDAK_VALID","pesan":"Body permintaan bukan JSON valid"}}
GET /pekerjaan -> 200 [{"id":1,"jenis":"kirim-email","data":{"penerima":"[email protected]","subjek":"Kwitansi"},"status":"menunggu","percobaan":0},{"id":2,"jenis":"buat-laporan","data":{"judul":"Laporan penjualan"},"status":"menunggu","percobaan":0}]
GET /pekerjaan/1 -> 200 {"id":1,"jenis":"kirim-email","data":{"penerima":"[email protected]","subjek":"Kwitansi"},"status":"menunggu","percobaan":0}
GET /pekerjaan/99 -> 404 {"error":{"kode":"PEKERJAAN_TIDAK_ADA","pesan":"Pekerjaan 99 tidak ditemukan"}}
DELETE /pekerjaan -> 405 {"error":{"kode":"METODE_TIDAK_DIIZINKAN","pesan":"Metode DELETE tidak didukung pada endpoint ini"}}Delapan baris output itu merangkum kontrak bagian ini: POST yang valid menjawab 201 dengan header Location, jenis asing ditolak 422 beserta detailnya, body yang bukan JSON ditolak 400, dan GET /pekerjaan menampilkan kedua pekerjaan masih menunggu — persis seperti yang dijanjikan, karena belum ada yang mengerjakan. Antrean dan pintu masuknya sudah berdiri; yang belum ada adalah tangan yang mengambil pekerjaan dari barisan. Itu pekerjaan Bagian 2.