// PERSONAL delivery worker. Given a QUEUED Notification outbox row, resolve its // channel and recipient address, invoke the channel adapter OUTSIDE any // transaction, then record the terminal status and audit trail in a short // transaction. The adapter is handed the row id as an idempotency key so a // redrain re-send of the same row collapses to a single provider-side delivery. import type { DB, Transaction } from "../generated/kysely-tailordb"; import type { DispatchAdapters } from "../lib/dispatchAdapters"; import { loadChannelById } from "../lib/dispatchQueries"; interface NotificationDeliveryRow { id: string; recipientUserId: string; channelId: string; subject: string; body: string; htmlBody: string | null; deliveryStatus: string; } export interface DeliverNotificationOutcome { deliveryStatus: "SENT" | "DELIVERED" | "FAILED" | "SKIPPED"; reason?: string; } type AuditEventType = "SENT" | "DELIVERED" | "FAILED"; async function writeAudit( db: Transaction, notificationId: string, eventType: AuditEventType, now: Date, errorClass: string | null = null, errorDetail: string | null = null, ) { await db .insertInto("NotificationDeliveryAudit") .values({ id: crypto.randomUUID(), notificationId, eventType, occurredAt: now, occurredBy: "system", errorClass, errorDetail, createdAt: now, updatedAt: now, }) .execute(); } // Each transition is conditional on the expected prior status so a concurrent // worker that already advanced the row is never overwritten. async function advance( db: Transaction, notificationId: string, patch: Record, now: Date, fromStatus: "QUEUED" | "SENT", ) { await db .updateTable("Notification") .set({ ...patch, updatedAt: now }) .where("id", "=", notificationId) .where("deliveryStatus", "=", fromStatus) .execute(); } async function recordFailure( db: DB, notificationId: string, now: Date, errorClass: string, errorDetail: string, ): Promise { await db.transaction().execute(async (trx) => { await advance(trx, notificationId, { deliveryStatus: "FAILED" }, now, "QUEUED"); await writeAudit(trx, notificationId, "FAILED", now, errorClass, errorDetail); }); return { deliveryStatus: "FAILED", reason: "CHANNEL_ADAPTER_FAILED" }; } export async function deliverNotification( db: DB, notificationId: string, adapters: DispatchAdapters, now: Date, ): Promise { const row = (await db .selectFrom("Notification") .selectAll() .where("id", "=", notificationId) .executeTakeFirst()) as NotificationDeliveryRow | undefined; if (!row || row.deliveryStatus !== "QUEUED") return { deliveryStatus: "SKIPPED" }; const channel = await loadChannelById(db, row.channelId); const channelKey = channel?.channelId; if (!channel || !channel.enabled || channel.kind === "DESTINATION" || !channelKey) { return recordFailure(db, row.id, now, "ChannelUnavailable", channelKey ?? row.channelId); } const profile = await adapters.resolveRecipient(db, row.recipientUserId); const recipientAddress = profile?.addresses[channelKey] ?? null; if (recipientAddress === null) { return recordFailure(db, row.id, now, "MissingRecipientAddress", channelKey); } if (channelKey === "IN_APP") { const sent = await adapters.inApp.send({ notificationId: row.id, idempotencyKey: row.id, recipientAddress, subject: row.subject, body: row.body, }); if (!sent.ok) { return recordFailure(db, row.id, now, sent.errorClass, sent.errorDetail); } await db.transaction().execute(async (trx) => { await advance(trx, row.id, { deliveryStatus: "SENT" }, now, "QUEUED"); await writeAudit(trx, row.id, "SENT", now); await advance(trx, row.id, { deliveryStatus: "DELIVERED" }, now, "SENT"); await writeAudit(trx, row.id, "DELIVERED", now); }); return { deliveryStatus: "DELIVERED" }; } if (channelKey === "EMAIL") { if (!adapters.email) { return recordFailure(db, row.id, now, "ChannelAdapterNotConfigured", "EMAIL"); } const sent = await adapters.email.send({ notificationId: row.id, idempotencyKey: row.id, recipientAddress, subject: row.subject, body: row.body, htmlBody: row.htmlBody, }); if (!sent.ok) { return recordFailure(db, row.id, now, sent.errorClass, sent.errorDetail); } await db.transaction().execute(async (trx) => { await advance( trx, row.id, { deliveryStatus: "SENT", adapterMessageId: sent.messageId }, now, "QUEUED", ); await writeAudit(trx, row.id, "SENT", now); }); return { deliveryStatus: "SENT" }; } // A PERSONAL channel with no wired adapter can never succeed; fail it so the // redrain worker stops retrying. return recordFailure(db, row.id, now, "ChannelAdapterNotConfigured", channelKey); }