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 a596f520b4 - Show all commits
+17 -2
View File
@@ -26,7 +26,7 @@
import { schedule } from 'node-cron'
import { and, eq, lte } from 'drizzle-orm'
import { db } from '../db/client.js'
import { calendarEvents, calendarOutbox, memberCredentials } from '../db/schema.js'
import { calendarEvents, calendarOutbox, calendars, memberCredentials } from '../db/schema.js'
import { createFastmailClient } from './client.js'
import { decryptPassword } from './crypto.js'
import { syncCalendar } from './sync.js'
@@ -201,11 +201,26 @@ async function dispatchRow(row: OutboxRow): Promise<DispatchResult> {
// 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.
// CR-02: calendar_events is keyed (calendarId, uid), and a shared Fastmail
// account (D-16) caches the same uid once per member's calendar. A uid-only
// re-read returns multiple rows and an arbitrary [0] — potentially the OTHER
// member's etag, which would spuriously 412 (false conflict → edit dropped,
// D-08) or coincidentally match and overwrite. Scope the re-read to THIS row's
// own calendar by joining through calendars on the outbox row's userId +
// calendarUrl so the freshest etag belongs to the writing member.
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 }>
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
.where(
and(
eq(calendarEvents.uid, row.uid),
eq(calendars.userId, row.userId),
eq(calendars.url, row.calendarUrl),
),
)
.limit(1)) as Array<{ etag: string | null }>
if (freshEtagRows.length > 0 && freshEtagRows[0].etag != null) {
etagForPut = freshEtagRows[0].etag
}