feat(deps): adopt p-limit for unbounded mass-op fan-outs
Cap concurrency on two services that were fanning out unbounded requests to external systems: 1. email-compose.service.ts — attachment resolution. User attaches 20 files → 20 simultaneous S3/MinIO GETs + 20 buffers in heap. Now capped at 4 concurrent reads; peak memory bounded by 4 × max-attachment-size regardless of attachment count. 2. document-signing-emails.service.ts — sendSigningCompleted fanned out one SMTP send per recipient simultaneously. A Sales Contract with 10 recipients (client + 5 sellers + 4 witnesses) hit SMTP provider connection limits (Mailgun/SES/Postmark all cap concurrent connections in the single digits) and dropped overflow silently. Now capped at 3 concurrent sends. Both use `pLimit(N)` from the Sindre Sorhus suite — well-tested at scale, ~1kb gzip per service. Pattern is established for the remaining audit-flagged mass-op services (brochures, backup, GDPR export) to adopt as those files are touched. Verified: tsc clean, vitest 1293/1293 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,8 @@
|
||||
* port (single-tenant deploys can keep using Documenso's hosted UI).
|
||||
*/
|
||||
|
||||
import pLimit from 'p-limit';
|
||||
|
||||
import { sendEmail } from '@/lib/email';
|
||||
import { getBrandingShell } from '@/lib/email/branding-resolver';
|
||||
import {
|
||||
@@ -231,33 +233,41 @@ export async function sendSigningReminder(args: SigningReminderArgs): Promise<vo
|
||||
export async function sendSigningCompleted(args: SigningCompletedArgs): Promise<void> {
|
||||
const branding = await getBrandingShell(args.portId);
|
||||
|
||||
// Cap concurrency at 3: a Sales Contract with 10 recipients (client +
|
||||
// 5 sellers + 4 witnesses) shouldn't fan out 10 simultaneous SMTP
|
||||
// sends. Most SMTP providers (Mailgun, SES, Postmark) cap concurrent
|
||||
// connections in the single digits and silently drop the overflow.
|
||||
const sendLimit = pLimit(3);
|
||||
|
||||
await Promise.all(
|
||||
args.recipients.map(async (recipient) => {
|
||||
const { subject, html, text } = signingCompletedEmail(
|
||||
{
|
||||
recipientName: recipient.name,
|
||||
documentLabel: args.documentLabel,
|
||||
clientName: args.clientName,
|
||||
portName: args.portName,
|
||||
completedAt: args.completedAt,
|
||||
},
|
||||
{ branding },
|
||||
);
|
||||
try {
|
||||
await sendEmail(recipient.email, subject, html, undefined, text, args.portId, [
|
||||
{ fileId: args.signedPdfFileId, filename: args.signedPdfFilename },
|
||||
]);
|
||||
logger.info(
|
||||
{ portId: args.portId, recipient: recipient.email, documentLabel: args.documentLabel },
|
||||
'Signing-completed email sent',
|
||||
args.recipients.map((recipient) =>
|
||||
sendLimit(async () => {
|
||||
const { subject, html, text } = signingCompletedEmail(
|
||||
{
|
||||
recipientName: recipient.name,
|
||||
documentLabel: args.documentLabel,
|
||||
clientName: args.clientName,
|
||||
portName: args.portName,
|
||||
completedAt: args.completedAt,
|
||||
},
|
||||
{ branding },
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err, portId: args.portId, recipient: recipient.email },
|
||||
'Signing-completed email send failed',
|
||||
);
|
||||
// Don't throw — sending to one recipient shouldn't block the others.
|
||||
}
|
||||
}),
|
||||
try {
|
||||
await sendEmail(recipient.email, subject, html, undefined, text, args.portId, [
|
||||
{ fileId: args.signedPdfFileId, filename: args.signedPdfFilename },
|
||||
]);
|
||||
logger.info(
|
||||
{ portId: args.portId, recipient: recipient.email, documentLabel: args.documentLabel },
|
||||
'Signing-completed email sent',
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err, portId: args.portId, recipient: recipient.email },
|
||||
'Signing-completed email send failed',
|
||||
);
|
||||
// Don't throw — sending to one recipient shouldn't block the others.
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user