fix(06): WR-06 bound post-write targeted resync with 10s timeout so a hang cannot wedge drain

This commit is contained in:
Lucas Berger
2026-06-10 16:53:13 -04:00
parent 9f88068d77
commit 0511a23886
+22 -1
View File
@@ -155,6 +155,14 @@ export function assembleRruleString(
*/ */
let isDraining = false let isDraining = false
/**
* WR-06: max time to wait on the post-write targeted re-sync before marking the
* outbox row 'done'. A stalled Fastmail connection cannot wedge the single-process
* drain loop beyond this cap; the PWA's next sync-status poll reconciles any cache
* that the timed-out re-sync did not refresh.
*/
const RESYNC_TIMEOUT_MS = 10_000
// ── Credential + client loading ────────────────────────────────────────────── // ── Credential + client loading ──────────────────────────────────────────────
/** /**
@@ -690,7 +698,20 @@ export async function runOutboxDrain(): Promise<void> {
// raced the re-sync and returned stale cache (deleted event still // raced the re-sync and returned stale cache (deleted event still
// present, edit not yet applied) — forcing a manual refresh. Re-syncing // present, edit not yet applied) — forcing a manual refresh. Re-syncing
// first means 'done' guarantees the cache already reflects the write. // first means 'done' guarantees the cache already reflects the write.
await triggerTargetedResync(row.calendarUrl, row.userId, clientCache) //
// WR-06: bound the re-sync with a timeout. triggerTargetedResync does
// unbounded network I/O against Fastmail (fetchCalendars + syncCalendar);
// a hang would leave this row 'pending' from the DB's view for the full
// duration, the 15s isDraining guard would no-op the next cycle, and the
// PWA would poll 'pending' indefinitely — wedging the single-process
// drain loop. On timeout we proceed to mark 'done' and let the PWA's next
// poll/refetch reconcile (the same documented refetch path the eager
// re-sync was optimizing). triggerTargetedResync already swallows its own
// errors, so the race only needs to cap the wait.
await Promise.race([
triggerTargetedResync(row.calendarUrl, row.userId, clientCache),
new Promise<void>((resolve) => setTimeout(resolve, RESYNC_TIMEOUT_MS)),
])
await db await db
.update(calendarOutbox) .update(calendarOutbox)
.set({ status: 'done' }) .set({ status: 'done' })