perf(audit-tier-3): bulk-fetch the five hot N+1 loops
Replaces per-row fan-out with grouped queries / inArray pre-fetches across the five dashboard + cron hotspots flagged in the audit (MED §13 / HIGH §11–14): * reminders.processFollowUpReminders — was 3 round trips per enabled-and-due interest. Now: filter in JS, single clients bulk-fetch, single reminders bulk-insert, single interests bulk-update, one summary socket emit. 1k due interests: 6 round trips total instead of 3000+. * portal.getClientInvoices — was a full-table scan filtered in JS. Now an inArray push-down on lower(billingEmail) + defensive limit(100). After 12mo this would have been the worst portal endpoint. * interest-scoring.calculateBulkScores — was 6N round trips (1 redis + 1 findFirst + 4 counts per interest). Now 4 grouped count queries on the port's interest set + a single redis pipeline to refresh the cache. 1k interests: ~7 round trips. * document-reminders.processReminderQueue — was 5N round trips per cron tick (port + template + lastReminder + pendingSigners + send per doc). Now hoists port + per-type template map + grouped lastReminder + bulk pendingSigners; per-row work collapses to a Map.get and the documenso send. 500 docs: ~7 round trips. * inquiry-notifications.sendInquiryNotifications — was sequential createNotification + emailQueue.add per recipient inside a public POST. Now Promise.all'd; a 20-user port stops blocking the public inquiry POST on ~80 round trips. Test status: 1168/1168 vitest, tsc clean. Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§11–14 (auditor-I Issues 1–4) + MED §13 (auditor-I Issue 5). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,33 +58,48 @@ export async function sendInquiryNotifications(params: InquiryNotificationParams
|
||||
logger.error({ err, interestId }, 'Failed to queue client confirmation email');
|
||||
}
|
||||
|
||||
// 2. Notify CRM users with interests.view permission on this port
|
||||
// 2. Notify CRM users with interests.view permission on this port.
|
||||
// The previous implementation `await`ed createNotification per user,
|
||||
// burning ≥3 DB round trips + 2 socket emits per call serially — a
|
||||
// port with 20 users meant ~80 round trips before this public POST
|
||||
// could even respond. Promise.all parallelises the DB writes; the
|
||||
// socket emit fan-out is the only thing that still scales linearly,
|
||||
// and that's a fire-and-forget local broadcast.
|
||||
try {
|
||||
const usersWithAccess = await findUsersWithInterestsPermission(portId);
|
||||
const crmUrl = `/${portSlug}/interests/${interestId}`;
|
||||
const description = `${clientFullName} has registered interest${
|
||||
mooringNumber ? ` in Berth ${mooringNumber}` : ''
|
||||
} via the website`;
|
||||
|
||||
for (const userId of usersWithAccess) {
|
||||
try {
|
||||
await createNotification({
|
||||
const settled = await Promise.allSettled(
|
||||
usersWithAccess.map((userId) =>
|
||||
createNotification({
|
||||
portId,
|
||||
userId,
|
||||
type: 'new_registration',
|
||||
title: 'New Interest Registered',
|
||||
description: `${clientFullName} has registered interest${mooringNumber ? ` in Berth ${mooringNumber}` : ''} via the website`,
|
||||
description,
|
||||
link: crmUrl,
|
||||
entityType: 'interest',
|
||||
entityId: interestId,
|
||||
dedupeKey: `inquiry-${interestId}`,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error({ err, userId, interestId }, 'Failed to create notification for user');
|
||||
}),
|
||||
),
|
||||
);
|
||||
for (const [i, r] of settled.entries()) {
|
||||
if (r.status === 'rejected') {
|
||||
logger.error(
|
||||
{ err: r.reason, userId: usersWithAccess[i], interestId },
|
||||
'Failed to create notification for user',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err, interestId }, 'Failed to notify CRM users');
|
||||
}
|
||||
|
||||
// 3. Notify external recipients
|
||||
// 3. Notify external recipients (parallel queue enqueues).
|
||||
try {
|
||||
const recipientsSetting = await getSetting('inquiry_notification_recipients', portId);
|
||||
const externalEmails: string[] = Array.isArray(recipientsSetting?.value)
|
||||
@@ -96,16 +111,18 @@ export async function sendInquiryNotifications(params: InquiryNotificationParams
|
||||
const appUrl = process.env.APP_URL ?? '';
|
||||
const crmUrl = `${appUrl}/${portSlug}/interests/${interestId}`;
|
||||
|
||||
for (const externalEmail of externalEmails) {
|
||||
await emailQueue.add('send-inquiry-sales-notification', {
|
||||
to: externalEmail,
|
||||
fullName: clientFullName,
|
||||
email: clientEmail,
|
||||
phone: clientPhone,
|
||||
mooringNumber,
|
||||
crmUrl,
|
||||
});
|
||||
}
|
||||
await Promise.all(
|
||||
externalEmails.map((externalEmail) =>
|
||||
emailQueue.add('send-inquiry-sales-notification', {
|
||||
to: externalEmail,
|
||||
fullName: clientFullName,
|
||||
email: clientEmail,
|
||||
phone: clientPhone,
|
||||
mooringNumber,
|
||||
crmUrl,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err, interestId }, 'Failed to notify external recipients');
|
||||
|
||||
Reference in New Issue
Block a user