feat(03-11): GREEN — re-read freshest calendarEvents etag before update PUT (WR-02)

- In update dispatch, SELECT etag FROM calendar_events WHERE uid = row.uid before PUT
- Use fresh etag as If-Match instead of stale enqueue-time row.etag when available
- Fall back to row.etag when calendarEvents has no matching row
- D-08 conflict detection intact: genuine external changes update calendarEvents.etag
  differently from any pending row, so they still 412 correctly
This commit is contained in:
Lucas Berger
2026-06-05 21:06:32 -04:00
parent 5eb26c0e6b
commit 09fd1f2e92
+18 -1
View File
@@ -193,11 +193,28 @@ async function dispatchRow(row: OutboxRow): Promise<DispatchResult> {
description: fields.description as string | undefined,
rruleString: fields.recurrence && fields.recurrence !== 'none' ? RRULE_PRESETS[fields.recurrence as string] : undefined,
})
// WR-02: re-read the freshest etag from calendarEvents just before PUT.
// Rapid successive edits to the same uid enqueue multiple update rows, each
// carrying the etag at enqueue time. If a prior edit succeeded and triggered
// a re-sync, calendarEvents.etag was updated but the next update row still
// carries the old enqueue-time etag — guaranteed 412 on the second edit.
// Using the freshest cached etag here prevents the spurious conflict toast
// while still preserving genuine conflict detection (D-08): a real external
// change updates calendarEvents.etag differently from any pending row's etag.
let etagForPut: string | null = row.etag ?? null
const freshEtagRows = (await db
.select({ etag: calendarEvents.etag })
.from(calendarEvents)
.where(eq(calendarEvents.uid, row.uid))) as Array<{ etag: string | null }>
if (freshEtagRows.length > 0 && freshEtagRows[0].etag != null) {
etagForPut = freshEtagRows[0].etag
}
response = await updateCalendarEvent(
client,
row.calendarObjectUrl,
icsString,
row.etag ?? null,
etagForPut,
)
} else {
// create