#!/usr/bin/env node const args = new Set(process.argv.slice(2)); const intervalSecondsRaw = process.env.CAMPAIGN_RETRY_DAEMON_INTERVAL_SECONDS?.trim(); const timeoutMsRaw = process.env.CAMPAIGN_RETRY_DAEMON_TIMEOUT_MS?.trim(); const runOnce = args.has("--once") || args.has("-1"); const shouldJitter = !args.has("--no-jitter"); const intervalMs = normalizePositiveNumber(intervalSecondsRaw, 300) * 1000; const requestTimeoutMs = normalizePositiveNumber(timeoutMsRaw, 30000); if (isNaN(intervalMs) || intervalMs <= 0) { console.error("Invalid CAMPAIGN_RETRY_DAEMON_INTERVAL_SECONDS"); process.exit(1); } const endpoint = resolveCampaignRetryEndpoint(); const token = process.env.CAMPAIGN_RETRY_JOB_TOKEN?.trim(); const payload = { ...(process.env.CAMPAIGN_RETRY_TENANT_ID?.trim() ? { tenantId: process.env.CAMPAIGN_RETRY_TENANT_ID.trim() } : {}), ...(process.env.CAMPAIGN_RETRY_CAMPAIGN_ID?.trim() ? { campaignId: process.env.CAMPAIGN_RETRY_CAMPAIGN_ID.trim() } : {}), ...(isPositiveInt(process.env.CAMPAIGN_RETRY_BATCH_SIZE) ? { recipientBatchSize: Number(process.env.CAMPAIGN_RETRY_BATCH_SIZE) } : {}), ...(isPositiveInt(process.env.CAMPAIGN_RETRY_MAX_CAMPAIGNS) ? { maxCampaigns: Number(process.env.CAMPAIGN_RETRY_MAX_CAMPAIGNS) } : {}) }; let isShuttingDown = false; let lastSummary = null; process.on("SIGINT", () => { isShuttingDown = true; console.info("\n[daemon] shutdown requested"); }); process.on("SIGTERM", () => { isShuttingDown = true; console.info("\n[daemon] shutdown requested"); }); (async function main() { if (runOnce) { await runLoopOnce(); } else { await runDaemon(); } })(); async function runDaemon() { console.info(`[daemon] starting campaign retry worker, interval=${intervalMs / 1000}s`); while (!isShuttingDown) { await runLoopOnce(); if (isShuttingDown) { break; } const jitter = shouldJitter ? Math.random() * Math.min(1000, intervalMs * 0.1) : 0; const waitMs = Math.max(1000, intervalMs + jitter); await sleep(waitMs); } if (lastSummary) { console.info(`[daemon] last run summary: ${JSON.stringify(lastSummary)}`); } console.info("[daemon] stopped"); } async function runLoopOnce() { const startedAt = new Date().toISOString(); const headers = { "content-type": "application/json" }; if (token) { headers.Authorization = `Bearer ${token}`; } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), requestTimeoutMs); try { const response = await fetch(endpoint, { method: "POST", headers, body: JSON.stringify(payload), signal: controller.signal }); const text = await response.text(); clearTimeout(timeout); if (!response.ok) { console.error(`[${startedAt}] campaign retry failed: ${response.status} ${response.statusText}`); console.error(text); lastSummary = { ts: startedAt, status: "failed", code: response.status, message: response.statusText }; if (runOnce) { process.exitCode = 1; } return; } lastSummary = safeParseJson(text) ?? { ts: startedAt, raw: text, status: "ok" }; console.log(`[${startedAt}] campaign retry done: ${typeof text === "string" ? text.slice(0, 250) : text}`); } catch (error) { clearTimeout(timeout); const message = error instanceof Error ? error.message : String(error); lastSummary = { ts: startedAt, status: "error", message }; console.error(`[${startedAt}] campaign retry request failed: ${message}`); if (runOnce) { process.exitCode = 1; } } if (runOnce) { process.exit(process.exitCode ?? 0); } } function resolveCampaignRetryEndpoint() { const baseUrl = process.env.CAMPAIGN_RETRY_JOB_URL?.trim() || process.env.APP_URL?.trim() || process.env.NEXT_PUBLIC_APP_URL?.trim(); if (!baseUrl) { console.error("Missing CAMPAIGN_RETRY_JOB_URL / APP_URL / NEXT_PUBLIC_APP_URL"); process.exit(1); } return baseUrl.endsWith("/api/jobs/campaign-retry") ? baseUrl : `${baseUrl.replace(/\/+$/, "")}/api/jobs/campaign-retry`; } function normalizePositiveNumber(value, fallback) { const raw = Number(value); if (!Number.isFinite(raw) || raw <= 0) { return fallback; } return raw; } function isPositiveInt(raw) { const value = Number(raw); return Number.isInteger(value) && value > 0; } function safeParseJson(raw) { try { return JSON.parse(raw); } catch { return null; } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }