mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
111 lines
4.9 KiB
JavaScript
111 lines
4.9 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* File Name : dispatch_email_broadcasts.cron.js
|
|
* Type : Cron Job
|
|
* Description : Sends real, paced SMTP email for queued email_broadcasts —
|
|
* the actual delivery half of controllers/admin/email_broadcasts
|
|
* .controller.js's createEmailBroadcast(), which only ever
|
|
* enqueues rows and returns immediately.
|
|
*
|
|
* Every tick, picks up to BATCH_SIZE 'pending' recipient rows
|
|
* (oldest broadcast first, FIFO within it) and sends them one
|
|
* at a time with a short delay between each — this is the "one
|
|
* by one, not a blocking for-loop in the API request" behavior:
|
|
* it's fine to block *here* because nothing is waiting on an
|
|
* HTTP response, and the delay keeps us well under Gmail SMTP's
|
|
* practical sustained-send pacing.
|
|
*
|
|
* Resumable by construction — if the process restarts mid-
|
|
* broadcast, the next tick just keeps consuming whatever rows
|
|
* are still 'pending'. Single-instance only: this does not use
|
|
* row-level locking, so running more than one app instance
|
|
* would let two ticks grab the same batch. Fine for the
|
|
* current single-process deployment; would need SELECT ... FOR
|
|
* UPDATE SKIP LOCKED before scaling horizontally.
|
|
*
|
|
* Schedule : Every minute ("* * * * *"). Registered by cron/admin.cron.js.
|
|
*
|
|
* Author: Kenneth Obsequio (@lash0000)
|
|
* Date Created: Jul. 3, 2026
|
|
***********************************************************************************************************************************************************************/
|
|
const mdl_EmailBroadcast = require('../../models/email_templates/email_broadcast.mdl');
|
|
const mdl_EmailBroadcastRecipient = require('../../models/email_templates/email_broadcast_recipient.mdl');
|
|
const mdl_EmailTemplate = require('../../models/email_templates/email_templates.mdl');
|
|
const { sendEmail } = require('../../services/email.service');
|
|
|
|
const BATCH_SIZE = 25;
|
|
const DELAY_MS = 600; // pacing between individual sends — keeps us well under Gmail's throttling threshold
|
|
|
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
async function run() {
|
|
try {
|
|
const recipients = await mdl_EmailBroadcastRecipient.findAll({
|
|
where: { status: 'pending' },
|
|
include: [{
|
|
model: mdl_EmailBroadcast,
|
|
as: 'broadcast',
|
|
where: { status: ['queued', 'sending'] },
|
|
include: [{ model: mdl_EmailTemplate, as: 'template' }],
|
|
}],
|
|
order: [['email_broadcast_id', 'ASC'], ['email_broadcast_recipient_id', 'ASC']],
|
|
limit: BATCH_SIZE,
|
|
});
|
|
|
|
if (!recipients.length) return;
|
|
|
|
const touchedBroadcastIds = new Set();
|
|
|
|
for (const recipient of recipients) {
|
|
const broadcast = recipient.broadcast;
|
|
const template = broadcast?.template;
|
|
|
|
if (!broadcast || !template) {
|
|
await recipient.update({ status: 'failed', error: 'Broadcast or template no longer exists.', sent_at: new Date() });
|
|
continue;
|
|
}
|
|
|
|
if (broadcast.status === 'queued') {
|
|
await broadcast.update({ status: 'sending', started_at: broadcast.started_at ?? new Date() });
|
|
}
|
|
touchedBroadcastIds.add(broadcast.email_broadcast_id);
|
|
|
|
try {
|
|
await sendEmail({
|
|
to: recipient.email,
|
|
type: template.type,
|
|
data: { name: recipient.name || 'there', email: recipient.email },
|
|
});
|
|
await recipient.update({ status: 'sent', sent_at: new Date() });
|
|
await broadcast.increment('sent_count');
|
|
} catch (err) {
|
|
await recipient.update({ status: 'failed', error: err.message, sent_at: new Date() });
|
|
await broadcast.increment('failed_count');
|
|
console.error('[CRON][EMAIL BROADCAST] Send failed:', recipient.email, err.message);
|
|
}
|
|
|
|
await delay(DELAY_MS);
|
|
}
|
|
|
|
// Close out any broadcast that has no pending recipients left.
|
|
for (const broadcastId of touchedBroadcastIds) {
|
|
const remaining = await mdl_EmailBroadcastRecipient.count({ where: { email_broadcast_id: broadcastId, status: 'pending' } });
|
|
if (remaining === 0) {
|
|
await mdl_EmailBroadcast.update(
|
|
{ status: 'completed', completed_at: new Date() },
|
|
{ where: { email_broadcast_id: broadcastId, status: ['queued', 'sending'] } }
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(`[CRON][EMAIL BROADCAST] Processed ${recipients.length} recipient(s) across ${touchedBroadcastIds.size} broadcast(s).`);
|
|
} catch (err) {
|
|
console.error('[CRON][EMAIL BROADCAST] Failed:', err);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
name: 'dispatchEmailBroadcasts',
|
|
schedule: '* * * * *',
|
|
run,
|
|
};
|