fix(05-review): NEW-WR-01 emit delete changes when server returns zero events (whole-cache clear)
Pre-capture all currently-cached rows into pendingDeleteRows before the whole-cache db.delete() when seenUids.length === 0. The existing >0 branch behavior is unchanged. Adds a regression test verifying onChanges receives one delete change per cached row on a full-calendar clear.
This commit is contained in:
@@ -256,6 +256,14 @@ export async function syncCalendar(
|
|||||||
notInArray(calendarEvents.uid, seenUids),
|
notInArray(calendarEvents.uid, seenUids),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
} else if (onChanges) {
|
||||||
|
// NEW-WR-01: server returned zero events → entire calendar cache will be cleared.
|
||||||
|
// Capture ALL currently-cached rows before the delete so delete-change events
|
||||||
|
// are emitted for each one. Without this, bulk/clear deletions were silently dropped.
|
||||||
|
pendingDeleteRows = await db
|
||||||
|
.select({ uid: calendarEvents.uid, title: calendarEvents.title })
|
||||||
|
.from(calendarEvents)
|
||||||
|
.where(eq(calendarEvents.calendarId, cal.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seenUids.length > 0) {
|
if (seenUids.length > 0) {
|
||||||
|
|||||||
@@ -365,4 +365,50 @@ describe('syncCalendar', () => {
|
|||||||
expect(mockDelete).toHaveBeenCalledTimes(1)
|
expect(mockDelete).toHaveBeenCalledTimes(1)
|
||||||
expect(mockDeleteWhere).toHaveBeenCalledTimes(1)
|
expect(mockDeleteWhere).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// NEW-WR-01: When the server returns zero events (whole-cache clear), the
|
||||||
|
// onChanges callback must still receive a 'delete' change for each cached row
|
||||||
|
// that is being pruned. Before this fix, the pendingDeleteRows pre-capture only
|
||||||
|
// ran inside the seenUids.length > 0 branch, so bulk/clear deletions silently
|
||||||
|
// dropped all delete change events.
|
||||||
|
it('NEW-WR-01: emits delete change events for each cached row when server returns zero events', async () => {
|
||||||
|
const { syncCalendar } = await import('../../src/broker/sync.js')
|
||||||
|
|
||||||
|
// First mockWhere call: calendar row lookup (ends in .limit(1)) → { limit: mockLimit }
|
||||||
|
// Second mockWhere call: pending-delete pre-capture (direct await, no .limit()) →
|
||||||
|
// must return an iterable array of cached rows. Use mockReturnValueOnce for the
|
||||||
|
// second call so it returns a resolved Promise<array> instead of { limit }.
|
||||||
|
//
|
||||||
|
// Call order when seenUids.length === 0 and onChanges is provided:
|
||||||
|
// 1. db.select().from(calendars).where(...).limit(1) — calendar row lookup
|
||||||
|
// 2. db.select({uid,title}).from(calendarEvents).where(...) — pending-delete capture (awaited directly)
|
||||||
|
mockWhere
|
||||||
|
.mockReturnValueOnce({ limit: mockLimit }) // call 1: calendar row lookup
|
||||||
|
.mockResolvedValueOnce([ // call 2: pending-delete capture
|
||||||
|
{ uid: 'uid-to-delete-1', title: 'Event A' },
|
||||||
|
{ uid: 'uid-to-delete-2', title: 'Event B' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
fetchCalendarObjects: vi.fn().mockResolvedValue([]),
|
||||||
|
}
|
||||||
|
const mockDavCal = {
|
||||||
|
url: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/',
|
||||||
|
displayName: 'Test Calendar',
|
||||||
|
ctag: 'ctag-empty',
|
||||||
|
syncToken: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectedChanges: import('../../src/lib/eventChangeDispatcher.js').EventChange[] = []
|
||||||
|
const onChanges = (changes: import('../../src/lib/eventChangeDispatcher.js').EventChange[]) => {
|
||||||
|
collectedChanges.push(...changes)
|
||||||
|
}
|
||||||
|
|
||||||
|
await syncCalendar(mockClient as never, mockDavCal as never, 1, onChanges)
|
||||||
|
|
||||||
|
// The callback must have received one delete change per cached row.
|
||||||
|
expect(collectedChanges).toHaveLength(2)
|
||||||
|
expect(collectedChanges[0]).toMatchObject({ uid: 'uid-to-delete-1', operation: 'delete' })
|
||||||
|
expect(collectedChanges[1]).toMatchObject({ uid: 'uid-to-delete-2', operation: 'delete' })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user