diff --git a/packages/iios-service/src/messaging/message.service.ts b/packages/iios-service/src/messaging/message.service.ts index 82091a7..7752796 100644 --- a/packages/iios-service/src/messaging/message.service.ts +++ b/packages/iios-service/src/messaging/message.service.ts @@ -208,28 +208,41 @@ export class MessageService { membersBy.set(p.threadId, [...(membersBy.get(p.threadId) ?? []), name]); } - const summaries = await Promise.all( - threads.map(async (t) => { - const last = await this.prisma.iiosInteraction.findFirst({ - where: { threadId: t.id }, - orderBy: { occurredAt: 'desc' }, - include: { parts: { where: { kind: 'TEXT' }, take: 1 } }, - }); - const members = membersBy.get(t.id) ?? []; - return { - threadId: t.id, - subject: t.subject, - membership: (t.metadata as { membership?: string } | null)?.membership, - metadata: (t.metadata as Record | null) ?? null, - participants: members, - participantCount: members.length, - unread: unreadBy.get(t.id) ?? 0, - muted: mutedBy.get(t.id) ?? false, - lastMessage: last?.parts[0]?.bodyText ?? undefined, - lastAt: last?.occurredAt, - }; - }), - ); + // Latest message per thread in ONE query (Postgres DISTINCT ON) instead of one findFirst + // per thread — a caller with many threads no longer fans out N parallel queries and + // exhausts the connection pool. + const lastRows = + threads.length === 0 + ? [] + : await this.prisma.$queryRaw>(Prisma.sql` + SELECT DISTINCT ON (i."threadId") + i."threadId" AS "threadId", + i."occurredAt" AS "occurredAt", + (SELECT p."bodyText" FROM "IiosMessagePart" p + WHERE p."interactionId" = i.id AND p.kind::text = 'TEXT' + ORDER BY p."partIndex" ASC LIMIT 1) AS "lastMessage" + FROM "IiosInteraction" i + WHERE i."threadId" IN (${Prisma.join(threads.map((t) => t.id))}) + ORDER BY i."threadId", i."occurredAt" DESC + `); + const lastBy = new Map(lastRows.map((r) => [r.threadId, r])); + + const summaries = threads.map((t) => { + const last = lastBy.get(t.id); + const members = membersBy.get(t.id) ?? []; + return { + threadId: t.id, + subject: t.subject, + membership: (t.metadata as { membership?: string } | null)?.membership, + metadata: (t.metadata as Record | null) ?? null, + participants: members, + participantCount: members.length, + unread: unreadBy.get(t.id) ?? 0, + muted: mutedBy.get(t.id) ?? false, + lastMessage: last?.lastMessage ?? undefined, + lastAt: last?.occurredAt, + }; + }); return summaries.sort((a, b) => (b.lastAt?.getTime() ?? 0) - (a.lastAt?.getTime() ?? 0)); }