import { setTimeout as sleep } from "node:timers/promises";
const BATCH_SIZE = 50;
function toItem(contact) {
const base = { id: contact.id };
if (contact.country) base.country = contact.country;
if (contact.firstName) return { ...base, type: "name", value: contact.firstName };
if (contact.email) return { ...base, type: "email", value: contact.email };
return { ...base, type: "username", value: contact.username };
}
async function enrich(contacts) {
const items = contacts.filter((c) => c.firstName || c.email || c.username).map(toItem);
for (let start = 0; start < items.length; start += BATCH_SIZE) {
const batch = items.slice(start, start + BATCH_SIZE);
const response = await fetch("https://api.finowl.co/v1/gender/batch", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FINOWL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ items: batch }),
signal: AbortSignal.timeout(40_000),
});
const body = await response.json();
if (response.status === 429 && body.error?.code === "rate_limited") {
await sleep(Number(response.headers.get("Retry-After") ?? 60) * 1000);
start -= BATCH_SIZE; // Not charged, so resend the same batch.
continue;
}
if (!Array.isArray(body.data)) {
// quota_exceeded, invalid_input or a service error. Stop and investigate.
throw new Error(body.error?.code ?? `http_${response.status}`);
}
for (const result of body.data) {
if (result.error) {
await markFailed(result.id, result.error.code);
} else {
await saveGender(result.id, result.data);
}
}
await sleep(1000); // Leave room in the rate limit for live traffic.
}
}