import { prisma } from "@/lib/prisma" /** * Menu scans are processed detached from the request, so a restart - which every * deploy causes - abandons anything in flight and leaves the row PROCESSING * forever, with a spinner that never resolves. * * There is no job queue to recover them, so instead they are reaped lazily whenever * a user looks at their scans. Cheap, needs no scheduler, and the worst case is a * stuck scan showing as failed a little later than it truly failed. */ const STUCK_AFTER_MS = 15 * 60 * 1000 export async function reapStuckScans(userId: string): Promise { try { const { count } = await prisma.menuScan.updateMany({ where: { userId, status: "PROCESSING", updatedAt: { lt: new Date(Date.now() - STUCK_AFTER_MS) }, }, data: { status: "FAILED", errorMessage: "Processing was interrupted. Please try scanning again.", }, }) if (count > 0) { console.warn(`[scan] reaped ${count} stuck scan(s) for ${userId}`) } return count } catch (error) { // Never let housekeeping break a page load. console.warn("[scan] reap failed:", error) return 0 } } /** * Caps concurrent menu extractions in this process. * * Each one is a vision call with a 4 minute timeout that costs real money, and the * work is detached from the request so nothing else bounds it. With one user this * never mattered; with several it is the difference between a queue and a stampede. */ const MAX_CONCURRENT_SCANS = 2 let inFlight = 0 export function tryAcquireScanSlot(): boolean { if (inFlight >= MAX_CONCURRENT_SCANS) return false inFlight += 1 return true } export function releaseScanSlot(): void { inFlight = Math.max(0, inFlight - 1) } export function scansInFlight(): number { return inFlight }