feat(05-07): implement eventChangeDispatcher + syncCalendar diff/title/onChanges

- Create eventChangeDispatcher.ts: dispatchEventChange + isMeaningfulChange
- D-04: description-only edits are silent; meaningful fields = title/dtstartUtc/dtstartDate/allDay/location
- D-03: actor excluded via ne() + application-level filter; all subs filtered by userId != actorUserId
- D-13: reads only push_subscriptions from MariaDB — no tsdav/Fastmail I/O
- syncCalendar: add optional onChanges callback; populate title from VEVENT SUMMARY on every upsert
- syncCalendar: pre-upsert SELECT to detect add vs update; track changedFields; prune emits deletes
- poller: pass onChanges with actor=cred.userId (external changes from other member)
- outboxWorker.triggerTargetedResync: pass onChanges with actor=userId (this-member writes)
- All 4 eventChangeDispatcher tests + 14 sync tests GREEN
This commit is contained in:
Lucas Berger
2026-06-09 22:03:52 -04:00
parent 4ef6333201
commit 30e9de13f9
4 changed files with 310 additions and 2 deletions
+13 -1
View File
@@ -22,6 +22,7 @@ import { memberCredentials, calendars } from '../db/schema.js'
import { decryptPassword } from './crypto.js'
import { createFastmailClient } from './client.js'
import { syncCalendar } from './sync.js'
import { dispatchEventChange } from '../lib/eventChangeDispatcher.js'
/**
* Runs one full poll cycle:
@@ -64,7 +65,18 @@ export async function runPoll(): Promise<void> {
continue
}
await syncCalendar(client, davCal, cred.userId)
// NOTIF-03: pass onChanges so external event changes push to non-actor members.
// actor = cred.userId (the member whose Fastmail poll detected the change — D-03).
await syncCalendar(client, davCal, cred.userId, (changes) => {
for (const change of changes) {
dispatchEventChange(change, cred.userId).catch((err: unknown) => {
console.error(
'[broker/poller] dispatchEventChange error:',
err instanceof Error ? err.message : String(err),
)
})
}
})
}
} catch (err) {
// Log the error but do NOT log the app password or key (T-03-04)