Milestone v1.0: FamilySync MVP #1

Merged
luckberg merged 376 commits from gsd/v1.0-milestone into main 2026-06-10 17:39:19 -04:00
Showing only changes of commit 09fd1f2e92 - Show all commits
+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