From 09fd1f2e921272cfde10e28937e85d230d8cd3eb Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 5 Jun 2026 21:06:32 -0400 Subject: [PATCH] =?UTF-8?q?feat(03-11):=20GREEN=20=E2=80=94=20re-read=20fr?= =?UTF-8?q?eshest=20calendarEvents=20etag=20before=20update=20PUT=20(WR-02?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/api/src/broker/outboxWorker.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/api/src/broker/outboxWorker.ts b/apps/api/src/broker/outboxWorker.ts index 085fe07..8f25bb2 100644 --- a/apps/api/src/broker/outboxWorker.ts +++ b/apps/api/src/broker/outboxWorker.ts @@ -193,11 +193,28 @@ async function dispatchRow(row: OutboxRow): Promise { 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