new commits

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-03 16:20:58 +08:00
parent 1372f4e975
commit 1d12f04967
93 changed files with 3849 additions and 1063 deletions
+110
View File
@@ -0,0 +1,110 @@
/***********************************************************************************************************************************************************************
* 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,
};