Files
pl_mail/tests/Jmap
Paul LütznerandClaude Opus 5 39dec36066 feat(calendar): the calendars themselves have a state too, and ifInState works
Calendar/get was the last method answering with the constant "fixed", left
that way when CalendarEvent/changes landed on the argument that the gap was
small: a user has a handful of calendars, Calendar/get returns all of them by
default, so re-running it is cheap. True about cost, and beside the point. A
state that never moves is a promise that nothing changed, and a client is
entitled to believe it — so renaming a calendar left the old name in the
sidebar until something unrelated forced a reload.

calendar_change_log now records the collections as well. event_id and
event_uid become nullable and their absence is what marks a row as being
about the calendar itself — one discriminator, because an explicit target
column beside a nullable id is a second thing to keep true and the two would
eventually disagree in silence. A sibling table was the alternative and was
rejected for costing a second sequence, a second repository and a second copy
of the token arithmetic, which is the part with the subtle guards in it and
therefore the last part worth having two of.

Every reader filters, and that filter is load-bearing. sync-collection and
CalendarEvent/changes take the rows with an event; Calendar/changes takes the
rest. A reader that forgot would report a renamed calendar as a changed event
and send a CalDAV client after an href built from a null UID, so both
directions are asserted rather than only the happy one.

App\Jmap\Calendar\CalendarState is deleted. Nothing answers with a constant
any more, so the class explaining why anything did has no subject; the
history it carried is folded into CalendarChangeLog and the listener, where
someone meeting the design will actually be standing.

ifInState is honoured rather than refused, and that reversal is the point
rather than a side effect. The refusal was correct while the state was a
constant — a guard that can never fail is worse than no guard, because a
client relies on it — and with a real token the promise is one the server can
keep. A client that read at one state and writes expecting it to still hold
now gets stateMismatch instead of being allowed to overwrite what it never
saw. Same protection If-Match gives a CalDAV client, from the same number.

One bug fell out, and it is the event tombstone's mirror image. queueCollection
deferred the calendar id to postFlush for every kind of change, which is right
for an insert that has not got one yet and wrong for a delete: Doctrine clears
the identifier once the row is gone, so the row recording a removal was the
one silently dropped. Taken eagerly now wherever there is one.

Also updates src/Jmap/README.md — 29 methods, and a note on why calendars
count in their own log.

No deployment change. One migration, relaxing two columns to nullable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 23:12:41 +02:00
..