import { and, eq, lt, or, isNull, sql } from 'drizzle-orm'; import { db } from '../db'; import { nudges } from './schema'; import type { NudgeSelect } from '../types'; /** Create or replace the schedule for one session. Re-arming resets the count — it is a new night. */ export async function setNudge(params: { userId: number; sessionId: string; message: string; everySeconds: number; maxCount: number; }): Promise { const [row] = await db .insert(nudges) .values({ ...params, enabled: true, sentCount: 0, lastFiredAt: null }) .onConflictDoUpdate({ target: [nudges.userId, nudges.sessionId], set: { message: params.message, everySeconds: params.everySeconds, maxCount: params.maxCount, enabled: true, sentCount: 0, lastFiredAt: null, updatedAt: new Date(), }, }) .returning(); return row!; } export async function getNudge(userId: number, sessionId: string): Promise { const [row] = await db .select() .from(nudges) .where(and(eq(nudges.userId, userId), eq(nudges.sessionId, sessionId))) .limit(1); return row ?? null; } export async function cancelNudge(userId: number, sessionId: string): Promise { await db.delete(nudges).where(and(eq(nudges.userId, userId), eq(nudges.sessionId, sessionId))); } /** * Everything the scheduler should consider this tick. * * Due means: enabled, under its ceiling, and either never fired or last fired at least `every_seconds` * ago. Whether the session is actually IDLE is not asked here — that lives in the sidecar, not the * database, and the scheduler checks it immediately before speaking. */ export async function dueNudges(now: Date = new Date()): Promise { return db .select() .from(nudges) .where( and( eq(nudges.enabled, true), lt(nudges.sentCount, nudges.maxCount), or( isNull(nudges.lastFiredAt), lt(nudges.lastFiredAt, sql`${now} - (${nudges.everySeconds} * interval '1 second')`), ), ), ); } /** One nudge delivered. Disables itself on reaching the ceiling rather than leaving a spent row armed. */ export async function recordNudgeSent(id: number): Promise { await db .update(nudges) .set({ sentCount: sql`${nudges.sentCount} + 1`, lastFiredAt: new Date(), updatedAt: new Date(), enabled: sql`${nudges.sentCount} + 1 < ${nudges.maxCount}`, }) .where(eq(nudges.id, id)); }