Milestone v1.0: FamilySync MVP #1

Merged
luckberg merged 376 commits from gsd/v1.0-milestone into main 2026-06-10 17:39:19 -04:00
Showing only changes of commit 0511a23886 - Show all commits
+22 -1
View File
@@ -155,6 +155,14 @@ export function assembleRruleString(
*/
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 ──────────────────────────────────────────────
/**
@@ -690,7 +698,20 @@ export async function runOutboxDrain(): Promise<void> {
// raced the re-sync and returned stale cache (deleted event still
// present, edit not yet applied) — forcing a manual refresh. Re-syncing
// 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
.update(calendarOutbox)
.set({ status: 'done' })