Tim otomatisasi yang menjalankan puluhan hingga ratusan task CAPTCHA per jam biasanya menabrak masalah yang sama: logger, penghitung metrik, dan retry handler semuanya butuh tahu kapan sebuah task berpindah status, tapi menumpuk logika itu langsung di dalam fungsi submit() membuat kode cepat kusut dan susah diuji terpisah. Event bus menyelesaikan ini dengan satu titik penyiaran — submitted, pending, solved, failed, timeout — yang bisa didengarkan oleh bagian mana pun dari aplikasi Anda tanpa mengubah kode penyelesaian CAPTCHA itu sendiri.
Kenapa Event Bus, Bukan Sekadar Callback atau Polling?
Callback URL dan polling res.php sudah cukup untuk mengetahui hasil akhir sebuah task. Masalahnya muncul begitu Anda perlu lebih dari sekadar "solved atau failed": dashboard operasional yang menampilkan task mana yang masih pending, alert yang menyala saat timeout terlalu sering terjadi, atau retry logic yang harus jalan otomatis tanpa memblokir task lain. Menambal semua kebutuhan itu langsung di fungsi submit membuat satu perubahan kecil — misalnya menambah metrik baru — berisiko merusak alur penyelesaian yang sudah stabil.
Skenario ini umum di agensi pemantauan harga e-commerce dan tim scraping freelance di Indonesia yang menjalankan worker CaptchaAI 24 jam di region cloud seperti AWS ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta). Ketika belasan proses submit task berjalan paralel, event bus jadi cara paling praktis untuk melacak status tiap task secara real-time tanpa membangun sistem logging terpisah dari nol — logger dan metrics listener tinggal didaftarkan, tanpa menyentuh kode submit() atau _poll() sama sekali.
Arsitektur Event Bus
[CaptchaBus]
├── emit("submitted", { taskId, type, pageurl })
├── emit("pending", { taskId, elapsed })
├── emit("solved", { taskId, solution, duration })
├── emit("failed", { taskId, error, duration })
└── emit("timeout", { taskId, elapsed })
↓ ↓ ↓
[Logger] [Metrics] [Retry Handler]
Setiap listener mendaftar secara independen ke event yang relevan saja. Logger mendengarkan semua lima event untuk audit trail, Metrics hanya butuh submitted, solved, dan failed untuk menghitung success rate, sedangkan Retry Handler cukup mendengarkan failed. Menambahkan fitur baru — misalnya notifikasi Slack saat timeout — tidak memerlukan satu baris pun perubahan pada kode submit() atau _poll().
Membangun Kelas CaptchaBus di JavaScript
Kelas CaptchaBus di bawah ini meng-extend EventEmitter bawaan Node.js. submit() mengirim task ke in.php, membuat taskId lokal untuk pelacakan, lalu memanggil _poll() di background — setiap perubahan status diteruskan lewat this.emit(...), bukan lewat return value atau exception.
const EventEmitter = require("events");
const axios = require("axios");
class CaptchaBus extends EventEmitter {
constructor(apiKey, options = {}) {
super();
this.apiKey = apiKey;
this.pollInterval = options.pollInterval || 5000;
this.maxWait = options.maxWait || 300000; // 5 minutes
this.pending = new Map();
}
async submit(params) {
const { method, sitekey, pageurl, ...extra } = params;
const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const submitParams = {
key: this.apiKey,
method: method || "userrecaptcha",
googlekey: sitekey,
pageurl: pageurl,
json: 1,
...extra,
};
try {
const resp = await axios.post(
"https://ocr.captchaai.com/in.php",
null,
{ params: submitParams }
);
if (resp.data.status !== 1) {
this.emit("failed", {
taskId,
error: resp.data.request,
duration: 0,
});
return null;
}
const captchaId = resp.data.request;
const startTime = Date.now();
this.emit("submitted", {
taskId,
captchaId,
method: method || "userrecaptcha",
pageurl,
});
// Start polling
this._poll(taskId, captchaId, startTime);
return taskId;
} catch (err) {
this.emit("failed", { taskId, error: err.message, duration: 0 });
return null;
}
}
async _poll(taskId, captchaId, startTime) {
const check = async () => {
const elapsed = Date.now() - startTime;
if (elapsed > this.maxWait) {
this.emit("timeout", { taskId, elapsed });
return;
}
this.emit("pending", { taskId, elapsed });
try {
const resp = await axios.get("https://ocr.captchaai.com/res.php", {
params: {
key: this.apiKey,
action: "get",
id: captchaId,
json: 1,
},
});
if (resp.data.status === 1) {
this.emit("solved", {
taskId,
captchaId,
solution: resp.data.request,
duration: Date.now() - startTime,
});
} else if (resp.data.request === "CAPCHA_NOT_READY") {
setTimeout(check, this.pollInterval);
} else {
this.emit("failed", {
taskId,
error: resp.data.request,
duration: Date.now() - startTime,
});
}
} catch (err) {
this.emit("failed", {
taskId,
error: err.message,
duration: Date.now() - startTime,
});
}
};
setTimeout(check, this.pollInterval);
}
}
module.exports = CaptchaBus;
Memasang Listener untuk Setiap Perubahan Status
Setelah kelas CaptchaBus siap, pasang listener sebanyak yang Anda butuhkan — satu untuk logging ke console, satu lagi untuk mengumpulkan metrics ke objek terpisah. Keduanya berjalan independen dan tidak saling tahu satu sama lain:
const CaptchaBus = require("./captcha-bus");
const bus = new CaptchaBus(process.env.CAPTCHAAI_API_KEY, {
pollInterval: 5000,
maxWait: 120000,
});
// Logging listener
bus.on("submitted", (e) => {
console.log(`[SUBMIT] ${e.taskId} → ${e.method} on ${e.pageurl}`);
});
bus.on("pending", (e) => {
console.log(`[PENDING] ${e.taskId} — ${(e.elapsed / 1000).toFixed(1)}s`);
});
bus.on("solved", (e) => {
console.log(
`[SOLVED] ${e.taskId} in ${(e.duration / 1000).toFixed(1)}s — ${e.solution.substring(0, 30)}...`
);
});
bus.on("failed", (e) => {
console.error(`[FAILED] ${e.taskId} — ${e.error}`);
});
bus.on("timeout", (e) => {
console.error(
`[TIMEOUT] ${e.taskId} after ${(e.elapsed / 1000).toFixed(1)}s`
);
});
// Metrics listener
const metrics = { submitted: 0, solved: 0, failed: 0, totalDuration: 0 };
bus.on("submitted", () => metrics.submitted++);
bus.on("solved", (e) => {
metrics.solved++;
metrics.totalDuration += e.duration;
});
bus.on("failed", () => metrics.failed++);
// Submit a CAPTCHA
bus.submit({
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com",
});
Versi Python: Pola Sama, Tanpa EventEmitter Bawaan
Node.js punya EventEmitter di modul inti, tapi Python tidak menyediakan setara langsung — jadi kelas CaptchaBus versi Python mengimplementasikan pola publish-subscribe sendiri lewat dict berisi daftar callback per event, plus threading.Thread untuk polling di background agar tidak memblokir thread utama:
import os
import time
import threading
from collections import defaultdict
import requests
class CaptchaBus:
def __init__(self, api_key, poll_interval=5, max_wait=300):
self.api_key = api_key
self.poll_interval = poll_interval
self.max_wait = max_wait
self._listeners = defaultdict(list)
def on(self, event, callback):
"""Register a listener for an event."""
self._listeners[event].append(callback)
return self
def emit(self, event, data):
"""Emit an event to all registered listeners."""
for callback in self._listeners.get(event, []):
try:
callback(data)
except Exception as e:
print(f"Listener error on {event}: {e}")
def submit(self, sitekey, pageurl, method="userrecaptcha", **extra):
"""Submit a CAPTCHA and begin tracking."""
task_id = f"task_{int(time.time())}_{id(sitekey) % 10000}"
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": method,
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1,
**extra
})
data = resp.json()
if data.get("status") != 1:
self.emit("failed", {
"task_id": task_id,
"error": data.get("request"),
"duration": 0
})
return None
captcha_id = data["request"]
start_time = time.time()
self.emit("submitted", {
"task_id": task_id,
"captcha_id": captcha_id,
"method": method,
"pageurl": pageurl
})
# Poll in a background thread
thread = threading.Thread(
target=self._poll,
args=(task_id, captcha_id, start_time),
daemon=True
)
thread.start()
return task_id
def _poll(self, task_id, captcha_id, start_time):
while True:
elapsed = time.time() - start_time
if elapsed > self.max_wait:
self.emit("timeout", {"task_id": task_id, "elapsed": elapsed})
return
time.sleep(self.poll_interval)
self.emit("pending", {"task_id": task_id, "elapsed": elapsed})
resp = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key,
"action": "get",
"id": captcha_id,
"json": 1
})
data = resp.json()
if data.get("status") == 1:
self.emit("solved", {
"task_id": task_id,
"solution": data["request"],
"duration": time.time() - start_time
})
return
elif data.get("request") != "CAPCHA_NOT_READY":
self.emit("failed", {
"task_id": task_id,
"error": data.get("request"),
"duration": time.time() - start_time
})
return
# Usage
bus = CaptchaBus(os.environ["CAPTCHAAI_API_KEY"])
bus.on("submitted", lambda e: print(f"[SUBMIT] {e['task_id']}"))
bus.on("solved", lambda e: print(f"[SOLVED] {e['task_id']} in {e['duration']:.1f}s"))
bus.on("failed", lambda e: print(f"[FAILED] {e['task_id']} — {e['error']}"))
bus.on("timeout", lambda e: print(f"[TIMEOUT] {e['task_id']}"))
bus.submit("6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", "https://example.com")
Retry Otomatis Cukup dengan Satu Listener Tambahan
Karena failed sudah disiarkan lewat event bus, retry logic tidak perlu ditanam di dalam submit() — cukup daftarkan listener baru yang memanggil ulang bus.submit() dengan parameter task asli, dibatasi maksimum 3 percobaan agar tidak retry tanpa henti:
// Automatic retry on failure
bus.on("failed", async (e) => {
if (e.retryCount >= 3) {
console.error(`[GIVE UP] ${e.taskId} after 3 retries`);
return;
}
console.log(`[RETRY] ${e.taskId} — attempt ${(e.retryCount || 0) + 1}`);
await bus.submit({
...e.originalParams,
_retryCount: (e.retryCount || 0) + 1,
});
});
Bungkus Event Bus dengan Promise API
Kalau kode pemanggil Anda lebih nyaman dengan await daripada mendengarkan event satu per satu, bungkus CaptchaBus di dalam sebuah Promise — event bus tetap jadi mesin di baliknya, hanya API di permukaan yang berubah:
function solveCaptcha(bus, params) {
return new Promise((resolve, reject) => {
const taskId = bus.submit(params);
function onSolved(e) {
if (e.taskId === taskId) {
cleanup();
resolve(e.solution);
}
}
function onFailed(e) {
if (e.taskId === taskId) {
cleanup();
reject(new Error(e.error));
}
}
function cleanup() {
bus.removeListener("solved", onSolved);
bus.removeListener("failed", onFailed);
bus.removeListener("timeout", onFailed);
}
bus.on("solved", onSolved);
bus.on("failed", onFailed);
bus.on("timeout", onFailed);
});
}
// Usage
const solution = await solveCaptcha(bus, {
sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com",
});
Masalah Umum dan Cara Mengatasinya
Berikut error yang paling sering muncul saat pertama kali mengintegrasikan event bus ini ke aplikasi produksi, beserta akar penyebab dan cara memperbaikinya:
| Masalah | Penyebab | Solusi |
|---|---|---|
| Listener tidak pernah terpanggil | Nama event salah ketik (mis. "solve" alih-alih "solved") |
Cocokkan ulang string event di emit() dan on() — JavaScript tidak memberi peringatan untuk nama event yang typo |
| Muncul peringatan memory leak di console | Terlalu banyak listener terdaftar pada satu event yang sama | Panggil setMaxListeners() untuk menaikkan batas, atau removeListener() setelah task selesai |
Console dibanjiri event pending |
pollInterval diset terlalu pendek (misal 500 ms) |
Naikkan pollInterval ke 5000 ms atau lebih — solve time reCAPTCHA v2 dan Turnstile umumnya tidak di bawah beberapa detik |
| Status task hilang saat retry | taskId baru dibuat setiap kali submit() dipanggil ulang |
Teruskan originalParams dari event failed supaya retry terhubung ke riwayat task yang sama |
| Listener terpanggil dua kali untuk task yang sama | Listener didaftarkan ulang tanpa dibersihkan lebih dulu (umum di hot-reload dev server) | Pastikan removeListener() dipanggil sebelum bus.on() yang baru, atau gunakan pola cleanup seperti pada promise wrapper |
Pertanyaan Umum
Apakah event bus ini menambah beban ke thread CaptchaAI saya?
Tidak. CaptchaBus murni logika di sisi aplikasi Anda — emit() dan on() tidak mengirim request apa pun ke API CaptchaAI. Konsumsi thread tetap ditentukan oleh berapa banyak task submit() yang berjalan bersamaan sesuai paket Anda (mis. BASIC 5 thread, STANDARD 15 thread), bukan oleh jumlah listener yang Anda daftarkan.
Perlukah saya pakai message broker seperti Kafka atau RabbitMQ, bukan EventEmitter bawaan?
Untuk aplikasi single-process, event bus in-process seperti pada panduan ini lebih sederhana dan tidak menambah komponen infrastruktur baru. Pertimbangkan Kafka, RabbitMQ, atau Redis Pub/Sub hanya kalau ada beberapa proses atau service terpisah yang sama-sama perlu bereaksi terhadap event CAPTCHA yang sama.
Bisakah satu CaptchaBus menangani reCAPTCHA v2, Turnstile, dan GeeTest v3 sekaligus?
Ya. Parameter method pada submit() menentukan tipe CAPTCHA per task (userrecaptcha, turnstile, geetest, dan seterusnya), sementara event yang di-emit — submitted, pending, solved, failed — sama untuk semua tipe. Listener logger dan metrics Anda tidak perlu tahu tipe CAPTCHA apa yang sedang diproses.
Bagaimana cara menyimpan riwayat event untuk audit atau debugging?
Tambahkan satu listener yang menulis setiap event ke file JSONL atau database, tanpa mengubah logika submit()/_poll() sama sekali. Ini membuat jejak audit yang berguna saat Anda perlu menelusuri kenapa sebuah task gagal beberapa jam sebelumnya.
Apa yang terjadi kalau listener tidak dibersihkan setelah task selesai?
Listener yang menumpuk tanpa removeListener() bisa memicu peringatan MaxListenersExceededWarning pada volume task tinggi, dan berpotensi memanggil callback lama untuk task yang sudah tidak relevan. Untuk task berumur pendek, ikuti pola cleanup pada promise wrapper di atas — daftarkan listener, lalu lepas begitu event solved/failed/timeout diterima.
Artikel Terkait
- Membangun pipeline CAPTCHA sisi klien dengan CaptchaAI
- Benchmark waktu solve CAPTCHA di CaptchaAI
- Notifikasi real-time lewat SSE untuk hasil solve
- Pola penanganan error pada callback CaptchaAI
Langkah Selanjutnya
Event bus ini adalah fondasi untuk pipeline CAPTCHA yang benar-benar event-driven — logger, metrics, dan retry handler tinggal ditambah sebagai listener baru kapan pun dibutuhkan, tanpa menyentuh kode solve yang sudah stabil. Ambil API key CaptchaAI Anda dan mulai pasang CaptchaBus di proyek Anda.