fix(05-review): WR-04 collect delete changes after db.delete to avoid race in sync fan-out

This commit is contained in:
Lucas Berger
2026-06-09 22:21:37 -04:00
parent 814d29dbdd
commit f058aefb88
+17 -8
View File
@@ -239,9 +239,15 @@ export async function syncCalendar(
// touches another calendar or the other household member's rows (BUG B).
// When the server returns zero events, prune the whole calendar's cache.
// NOTIF-03: collect deleted uids for the onChanges callback.
//
// WR-04: pre-capture the rows to delete BEFORE the DB delete so we know what
// was removed, then push changes AFTER the delete completes. This ensures the
// onChanges payload only describes events that are truly gone from the cache —
// not events that may have been re-fetched in a concurrent poll.
let pendingDeleteRows: Array<{ uid: string; title: string | null }> = []
if (onChanges && seenUids.length > 0) {
// Find cached uids that are about to be pruned so we can emit delete changes
const cachedRows = await db
pendingDeleteRows = await db
.select({ uid: calendarEvents.uid, title: calendarEvents.title })
.from(calendarEvents)
.where(
@@ -250,13 +256,6 @@ export async function syncCalendar(
notInArray(calendarEvents.uid, seenUids),
),
)
for (const row of cachedRows) {
changes.push({
uid: row.uid,
title: row.title ?? null,
operation: 'delete',
})
}
}
if (seenUids.length > 0) {
@@ -267,6 +266,16 @@ export async function syncCalendar(
await db.delete(calendarEvents).where(eq(calendarEvents.calendarId, cal.id))
}
// Collect delete changes AFTER the DB delete (WR-04: avoids race where the same
// uid is re-inserted by a concurrent poll before onChanges fires).
for (const row of pendingDeleteRows) {
changes.push({
uid: row.uid,
title: row.title ?? null,
operation: 'delete',
})
}
// 6. Fire onChanges callback if provided and there are changes (NOTIF-03).
// Fire-and-forget: sync correctness must not depend on push success.
if (onChanges && changes.length > 0) {