diff --git a/backend/src/routes/intake.ts b/backend/src/routes/intake.ts index 62db46c..d3542e3 100644 --- a/backend/src/routes/intake.ts +++ b/backend/src/routes/intake.ts @@ -4,6 +4,7 @@ import { asyncRoute } from '../asyncRoute'; import { hashToken } from '../uploadLinks'; import { uploadImages, verifyUploadedImages, insertItemImages } from '../imageUpload'; import { intakeViewLimiter, intakeSubmitLimiter } from '../rateLimit'; +import { draftQueued } from '../intake/draftingWorker'; const router = Router(); @@ -148,6 +149,14 @@ router.post( } await client.query('COMMIT'); + + // Deliberately not awaited, and catching for itself. A slow or failing + // model must not become a failed upload for someone who did nothing + // wrong, which is the whole reason drafting does not happen inline. The + // sweeper picks up anything this misses, so the cost of it failing here + // is a few minutes' delay rather than a lost submission. + void draftQueued(1).catch((err) => console.error('[drafting] after submission:', err)); + // No item id in the response: the sender has no business knowing about // the catalogue, and nothing they could do with it. res.status(201).json({ ok: true }); diff --git a/backend/src/server.ts b/backend/src/server.ts index af7d9cb..0c89560 100755 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -6,6 +6,7 @@ import { renderTemplate, greeting, formatDuration } from './emailTemplates'; import { getSettings } from './adminSettings'; import { loadStoredTemplate } from './routes/adminEmailTemplates'; import { validateEnv } from './envValidation'; +import { draftQueued } from './intake/draftingWorker'; // Release cart holds whose expiry has passed. async function sweepExpiredCarts(): Promise { @@ -96,6 +97,16 @@ interface ReminderRow { setInterval(() => void sweepExpiredCarts(), 5 * 60 * 1000); cron.schedule('0 9 * * *', () => void sendCartReminders()); +// Every five minutes, in the same shape as the cart sweep. This is what makes a +// restart mid-draft recoverable rather than a permanently stalled row, and what +// picks up anything the post-submission call missed. Unlike the two above, +// draftQueued does not catch at its own top level — the initial query can +// reject — so it catches here instead, for the reason the comment above gives. +setInterval( + () => void draftQueued().catch((err) => console.error('[drafting] sweep:', err)), + 5 * 60 * 1000 +); + const PORT = parseInt(process.env.PORT || '3000', 10); // Checked at boot rather than left to be discovered by the first request that