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

@@ -427,27 +427,37 @@ export async function processFollowUpReminders() {
const now = new Date();
for (const interest of enabledInterests) {
if (!interest.reminderDays) continue;
// Check if enough days have passed since last activity
// Pick the interests whose follow-up window has elapsed. Pre-filtering
// here means the per-row N+1 walk that used to issue (1 client lookup
// + 1 reminder insert + 1 interest update) per interest is replaced by
// a single client-bulk-fetch + a single reminder bulk-insert + a
// single interests bulk-update against an `inArray` set.
const dueInterests = enabledInterests.filter((interest) => {
if (!interest.reminderDays) return false;
const lastActivity = interest.reminderLastFired ?? interest.updatedAt;
const daysSinceActivity = (now.getTime() - lastActivity.getTime()) / (1000 * 60 * 60 * 24);
return daysSinceActivity >= interest.reminderDays;
});
if (daysSinceActivity < interest.reminderDays) continue;
if (dueInterests.length === 0) continue;
// Get client name for the reminder title
const client = interest.clientId
? await db.query.clients.findFirst({ where: eq(clients.id, interest.clientId) })
: null;
const clientIds = Array.from(
new Set(dueInterests.map((i) => i.clientId).filter((v): v is string => Boolean(v))),
);
const clientsByIdEntries =
clientIds.length > 0
? await db
.select({ id: clients.id, fullName: clients.fullName })
.from(clients)
.where(inArray(clients.id, clientIds))
: [];
const clientById = new Map(clientsByIdEntries.map((c) => [c.id, c]));
const title = client ? `Follow up with ${client.fullName}` : 'Follow up on interest';
// Find the assigned user (first userPortRole for this port, or fallback)
// For now, leave assignedTo null - the notification goes to the port room
await db.insert(reminders).values({
const newReminders = dueInterests.map((interest) => {
const client = interest.clientId ? clientById.get(interest.clientId) : null;
return {
portId: port.id,
title,
title: client ? `Follow up with ${client.fullName}` : 'Follow up on interest',
note: 'Auto-generated: no activity detected within the configured follow-up window.',
dueAt: now,
priority: 'medium',
@@ -456,23 +466,39 @@ export async function processFollowUpReminders() {
interestId: interest.id,
clientId: interest.clientId,
autoGenerated: true,
});
};
});
// Update last fired timestamp
if (newReminders.length > 0) {
await db.insert(reminders).values(newReminders);
await db
.update(interests)
.set({ reminderLastFired: now })
.where(eq(interests.id, interest.id));
.where(
inArray(
interests.id,
dueInterests.map((i) => i.id),
),
);
}
// Fire notification to the port room
// Single port-room emit summarising the batch — the per-row emit was
// mostly noise to the dashboard and amplified socket traffic linearly
// with interest count.
if (newReminders.length > 0) {
emitToRoom(`port:${port.id}`, 'system:alert', {
alertType: 'follow_up_created',
message: title,
message: `${newReminders.length} follow-up reminder${
newReminders.length === 1 ? '' : 's'
} created`,
severity: 'info',
});
logger.info({ interestId: interest.id, portId: port.id }, 'Auto follow-up reminder created');
}
logger.info(
{ portId: port.id, created: newReminders.length },
'Auto follow-up reminders created (bulk)',
);
}
}