Milestone v1.0: FamilySync MVP #1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user