fix(05-review): WR-04 collect delete changes after db.delete to avoid race in sync fan-out
This commit is contained in:
@@ -239,9 +239,15 @@ export async function syncCalendar(
|
|||||||
// touches another calendar or the other household member's rows (BUG B).
|
// touches another calendar or the other household member's rows (BUG B).
|
||||||
// When the server returns zero events, prune the whole calendar's cache.
|
// When the server returns zero events, prune the whole calendar's cache.
|
||||||
// NOTIF-03: collect deleted uids for the onChanges callback.
|
// 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) {
|
if (onChanges && seenUids.length > 0) {
|
||||||
// Find cached uids that are about to be pruned so we can emit delete changes
|
// 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 })
|
.select({ uid: calendarEvents.uid, title: calendarEvents.title })
|
||||||
.from(calendarEvents)
|
.from(calendarEvents)
|
||||||
.where(
|
.where(
|
||||||
@@ -250,13 +256,6 @@ export async function syncCalendar(
|
|||||||
notInArray(calendarEvents.uid, seenUids),
|
notInArray(calendarEvents.uid, seenUids),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for (const row of cachedRows) {
|
|
||||||
changes.push({
|
|
||||||
uid: row.uid,
|
|
||||||
title: row.title ?? null,
|
|
||||||
operation: 'delete',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seenUids.length > 0) {
|
if (seenUids.length > 0) {
|
||||||
@@ -267,6 +266,16 @@ export async function syncCalendar(
|
|||||||
await db.delete(calendarEvents).where(eq(calendarEvents.calendarId, cal.id))
|
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).
|
// 6. Fire onChanges callback if provided and there are changes (NOTIF-03).
|
||||||
// Fire-and-forget: sync correctness must not depend on push success.
|
// Fire-and-forget: sync correctness must not depend on push success.
|
||||||
if (onChanges && changes.length > 0) {
|
if (onChanges && changes.length > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user