@@ -10,6 +10,11 @@ import {
1010 setIntervalAsync ,
1111} from 'set-interval-async/dynamic'
1212import { prisma } from './db.server.ts'
13+ import { getrecipientsforcron } from './prisma-generated.server/sql.ts'
14+ import {
15+ NEXT_SCHEDULE_SENTINEL_DATE ,
16+ PREV_SCHEDULE_SENTINEL_DATE ,
17+ } from './schedule-constants.server.ts'
1318import { sendText , sendTextToRecipient } from './text.server.ts'
1419
1520export class CronParseError extends Error {
@@ -68,47 +73,40 @@ export async function sendNextTexts() {
6873 const reminderWindowMs = 1000 * 60 * 30
6974 const reminderCutoff = new Date ( now . getTime ( ) + reminderWindowMs )
7075
71- const recipients = await prisma . recipient . findMany ( {
72- where : {
73- verified : true ,
74- disabled : false ,
75- user : { stripeId : { not : null } } ,
76- OR : [
77- { nextScheduledAt : { lte : reminderCutoff } } ,
78- { nextScheduledAt : null } ,
79- ] ,
80- } ,
81- select : {
82- id : true ,
83- name : true ,
84- scheduleCron : true ,
85- timeZone : true ,
86- prevScheduledAt : true ,
87- nextScheduledAt : true ,
88- lastRemindedAt : true ,
89- lastSentAt : true ,
90- user : {
91- select : {
92- phoneNumber : true ,
93- name : true ,
94- } ,
95- } ,
76+ // Optimized TypedSQL query that:
77+ // 1. Uses INNER JOIN (we filter by User.stripeId, so LEFT JOIN is unnecessary)
78+ // 2. Uses the composite index on (verified, disabled, nextScheduledAt, userId)
79+ // 3. Handles both regular scheduled recipients and those with sentinel dates
80+ // Note: NEXT_SCHEDULE_SENTINEL_DATE (9999-12-31) will always be > reminderCutoff,
81+ // so recipients with invalid schedules won't be included
82+ const recipients = await prisma . $queryRawTyped (
83+ getrecipientsforcron ( reminderCutoff ) ,
84+ )
85+
86+ // Transform the raw result to match the expected format
87+ const formattedRecipients = recipients . map ( ( r ) => ( {
88+ ...r ,
89+ user : {
90+ phoneNumber : r . userPhoneNumber ,
91+ name : r . userName ,
9692 } ,
97- } )
93+ } ) )
9894
99- if ( ! recipients . length ) return
95+ if ( ! formattedRecipients . length ) return
10096
10197 let dueSentCount = 0
10298 let reminderSentCount = 0
103- for ( const recipient of recipients ) {
99+ for ( const recipient of formattedRecipients ) {
104100 let scheduleWindow : {
105101 prevScheduledAt : Date
106102 nextScheduledAt : Date
107- } | null = null
103+ }
108104 if (
109105 recipient . prevScheduledAt &&
110106 recipient . nextScheduledAt &&
111- recipient . nextScheduledAt > now
107+ recipient . nextScheduledAt > now &&
108+ recipient . nextScheduledAt . getTime ( ) !==
109+ NEXT_SCHEDULE_SENTINEL_DATE . getTime ( )
112110 ) {
113111 scheduleWindow = {
114112 prevScheduledAt : recipient . prevScheduledAt ,
@@ -126,6 +124,14 @@ export async function sendNextTexts() {
126124 `Invalid cron string "${ recipient . scheduleCron } " for recipient ${ recipient . id } :` ,
127125 error instanceof Error ? error . message : error ,
128126 )
127+ // Update with sentinel dates to prevent repeated processing
128+ await prisma . recipient . update ( {
129+ where : { id : recipient . id } ,
130+ data : {
131+ prevScheduledAt : PREV_SCHEDULE_SENTINEL_DATE ,
132+ nextScheduledAt : NEXT_SCHEDULE_SENTINEL_DATE ,
133+ } ,
134+ } )
129135 continue
130136 }
131137 }
0 commit comments