const queue = require('../queue/queue'); const config = require('../config/config'); const client = require('./resultClient'); const transientCodes = new Set(['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'EAI_AGAIN', 'ENETUNREACH']); function buildNextAttempt(attempts) { const schedule = config.retries.schedule; const index = Math.min(attempts - 1, schedule.length - 1); const delaySeconds = schedule[index] || schedule[schedule.length - 1] || 60; return Math.floor(Date.now() / 1000) + delaySeconds; } function isTransientError(err) { if (!err) return true; if (err.code && transientCodes.has(err.code)) return true; if (err.message && err.message.toLowerCase().includes('timeout')) return true; return false; } async function deliverOutboxEntry(entry) { const payload = JSON.parse(entry.canonical_payload); const attemptNumber = entry.attempts + 1; let response; let attemptStatus = 'failure'; try { response = await client.deliver(payload); attemptStatus = response.code >= 200 && response.code < 300 ? 'success' : 'failure'; await queue.recordDeliveryAttempt({ outboxId: entry.id, attempt: attemptNumber, status: attemptStatus, responseCode: response.code, responseBody: response.body, latency: response.latency }); if (attemptStatus === 'success') { await queue.markOutboxStatus(entry.id, 'processed', { attempts: attemptNumber, lastError: null, nextAttemptAt: Math.floor(Date.now() / 1000) }); return; } if (response.code === 400 || response.code === 422) { await queue.markOutboxStatus(entry.id, 'dead_letter', { attempts: attemptNumber, lastError: `HTTP ${response.code}`, nextAttemptAt: Math.floor(Date.now() / 1000) }); await queue.moveToDeadLetter(payload, `HTTP ${response.code}`); return; } if (attemptNumber >= config.retries.maxAttempts) { await queue.markOutboxStatus(entry.id, 'dead_letter', { attempts: attemptNumber, lastError: response.body, nextAttemptAt: Math.floor(Date.now() / 1000) }); await queue.moveToDeadLetter(payload, response.body); return; } const nextAttemptAt = buildNextAttempt(attemptNumber); await queue.markOutboxStatus(entry.id, 'retrying', { attempts: attemptNumber, lastError: `HTTP ${response.code}`, nextAttemptAt }); } catch (error) { await queue.recordDeliveryAttempt({ outboxId: entry.id, attempt: attemptNumber, status: 'failure', responseCode: null, responseBody: error.message, latency: null }); const shouldDeadLetter = attemptNumber >= config.retries.maxAttempts || !isTransientError(error); if (shouldDeadLetter) { await queue.markOutboxStatus(entry.id, 'dead_letter', { attempts: attemptNumber, lastError: error.message, nextAttemptAt: Math.floor(Date.now() / 1000) }); await queue.moveToDeadLetter(payload, error.message); return; } const nextAttemptAt = buildNextAttempt(attemptNumber); await queue.markOutboxStatus(entry.id, 'retrying', { attempts: attemptNumber, lastError: error.message, nextAttemptAt }); } } module.exports = { deliverOutboxEntry };