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:
Matt Ciaccio
2026-05-05 20:41:23 +02:00
parent d3a6a9beef
commit 7854cbabe4
5 changed files with 351 additions and 64 deletions

View File

@@ -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');