fix(03): CR-02 scope freshest-etag re-read to the writing member's calendar
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user