chore: initial project import
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled

This commit is contained in:
Wira Basalamah
2026-04-21 09:29:29 +07:00
commit adde003fba
222 changed files with 37657 additions and 0 deletions

View File

@ -0,0 +1,159 @@
#!/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));
}