Read these targets and merge the answer into the map. A no-op for an empty list.
Every id asked about is present when this resolves, as NO_REACTIONS if the reply did
not mention it — the transport may omit an empty summary, this may not. Ids nobody asked
about are left alone, which is what makes a second page merge rather than replace.
The invariant is load-bearing, not tidiness. Without it undefined means both "never
asked" and "asked, and it has nothing", a target whose last reaction was removed keeps its
chips forever, and toggle cannot roll back — merging a reply that omits the target leaves
the entry optimism just painted exactly where it was.
Add the caller's reaction, or take it back if it is already theirs. Optimistic, then reconciled: a failed write refetches the one target and rethrows, so the caller can toast. The optimistic entry does not outlive a refused write, whatever the refetch answers.
Reaction summaries keyed by target id, plus the two verbs that change them.
A
Readable, not aWritable: everything that writes is on this interface, so there is no path by which a component can set a count the server has not agreed to.