Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .cursor/rules/agents.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Agent instructions

- Use typed SQL (generated `sql` helpers with `prisma.$queryRawTyped`).
- Do not use `prisma.$queryRaw` or `Prisma.sql`.
- Exception: Prisma typed SQL filenames must be valid JS identifiers (no dashes), so the lower-kebab-case rule does not apply under `prisma/sql/`.
23 changes: 20 additions & 3 deletions app/routes/_app+/recipients+/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { requireUserId } from '#app/utils/auth.server.js'
import { CronParseError, getScheduleWindow } from '#app/utils/cron.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { getunsentmessagecounts } from '#app/utils/prisma-generated.server/sql.ts'
import {
NEXT_SCHEDULE_SENTINEL_DATE,
PREV_SCHEDULE_SENTINEL_DATE,
Expand Down Expand Up @@ -36,6 +37,7 @@ function formatScheduleDisplay(date: Date, timeZone: string) {

export async function loader({ request }: LoaderFunctionArgs) {
const userId = await requireUserId(request)

const recipients = await prisma.recipient.findMany({
select: {
id: true,
Expand All @@ -46,10 +48,25 @@ export async function loader({ request }: LoaderFunctionArgs) {
disabled: true,
prevScheduledAt: true,
nextScheduledAt: true,
_count: { select: { messages: { where: { sentAt: null } } } },
},
where: { userId },
where: {
userId,
},
orderBy: { id: 'asc' },
})
const recipientIds = recipients.map((recipient) => recipient.id)
const messageCounts = recipientIds.length
? await prisma.$queryRawTyped(
getunsentmessagecounts(JSON.stringify(recipientIds)),
)
: []
const messageCountByRecipientId = new Map(
messageCounts.map((row) => [row.recipientId, Number(row.unsentCount ?? 0)]),
)
const recipientsWithCounts = recipients.map((recipient) => ({
...recipient,
messageCount: messageCountByRecipientId.get(recipient.id) ?? 0,
}))

const now = new Date()
const scheduleUpdates: Array<{
Expand All @@ -59,7 +76,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
}> = []

// Ensure we have a schedule window for sorting/display
const sortedRecipients = recipients
const sortedRecipients = recipientsWithCounts
.map((recipient) => {
let nextScheduledAt = recipient.nextScheduledAt
let prevScheduledAt = recipient.prevScheduledAt
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_app+/recipients+/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default function RecipientsIndexRoute() {
{hasRecipients ? (
<div className="md:divide-border space-y-4 md:space-y-0 md:divide-y">
{recipients.map((recipient) => {
const messageCount = recipient._count.messages
const messageCount = recipient.messageCount
const messageLabel = messageCount === 1 ? 'message' : 'messages'
const messageText = `${messageCount} ${messageLabel}`
const messagePreparedText = `${messageText} prepared`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- CreateIndex
CREATE INDEX "recipient_by_user" ON "Recipient"("userId", "id");

-- CreateIndex
CREATE INDEX "message_unsent_by_recipient" ON "Message"("recipientId")
WHERE "sentAt" IS NULL;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ model Recipient {

// non-unique foreign key
@@index([userId])
@@index([userId, id], name: "recipient_by_user")
// Optimized composite index for cron query: equality cols first, then range, then join
@@index([verified, disabled, nextScheduledAt, userId], name: "Recipient_cron_query_idx")
}
Expand Down
10 changes: 10 additions & 0 deletions prisma/sql/getunsentmessagecounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Count unsent messages for a list of recipient IDs.
--
-- @param {String} $1 - JSON array of recipient ids
SELECT
recipientId,
CAST(COUNT(*) AS INTEGER) AS unsentCount
FROM Message INDEXED BY message_unsent_by_recipient
WHERE sentAt IS NULL
AND recipientId IN (SELECT value FROM json_each($1))
GROUP BY recipientId;
29 changes: 25 additions & 4 deletions scripts/benchmark-performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { performance } from 'node:perf_hooks'
import { parseArgs } from 'node:util'
import { CronParseError, getScheduleWindow } from '#app/utils/cron.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { getrecipientsforcron } from '#app/utils/prisma-generated.server/sql.ts'
import {
getrecipientsforcron,
getunsentmessagecounts,
} from '#app/utils/prisma-generated.server/sql.ts'
import { NEXT_SCHEDULE_SENTINEL_DATE } from '#app/utils/schedule-constants.server.ts'

const MESSAGES_PER_PAGE = 100
const RECIPIENTS_PAGE_SIZE = 200

type Summary = {
min: number
Expand Down Expand Up @@ -120,14 +124,31 @@ async function benchmarkRecipientsList(
disabled: true,
prevScheduledAt: true,
nextScheduledAt: true,
_count: { select: { messages: { where: { sentAt: null } } } },
},
orderBy: { id: 'asc' },
take: RECIPIENTS_PAGE_SIZE,
})
const recipientIds = recipients.map((recipient) => recipient.id)
const messageCounts = recipientIds.length
? await prisma.$queryRawTyped(
getunsentmessagecounts(JSON.stringify(recipientIds)),
)
: []
const messageCountByRecipientId = new Map(
messageCounts.map((row) => [
row.recipientId,
Number(row.unsentCount ?? 0),
]),
)
const recipientsWithCounts = recipients.map((recipient) => ({
...recipient,
messageCount: messageCountByRecipientId.get(recipient.id) ?? 0,
}))
const queryMs = performance.now() - queryStart

const computeStart = performance.now()
let errors = 0
const sortedRecipients = recipients
const sortedRecipients = recipientsWithCounts
.map((recipient) => {
try {
const isSentinel =
Expand Down Expand Up @@ -184,7 +205,7 @@ async function benchmarkRecipientsList(
querySamples.push(queryMs)
computeSamples.push(computeMs)
cronErrors.push(errors)
lastCount = recipients.length
lastCount = recipientsWithCounts.length
}

return {
Expand Down
Loading