Kind 3 already is a CRDT in the narrow sense: a last-writer-wins register per pubkey, ordered by created_at. That is exactly why it loses data silently. Newest write wins, and it cannot tell 'the event never reached me' apart from 'I unfollowed'. Your example needs the piece kind 3 lacks: a version vector or per-entry tombstones. Add tombstones and the merge is well defined. Skip them and any union merge resurrects unfollows, so clients that tried ended up with special casing. My reservation sits on the read side. One kind 3 is a single self-contained event. A follow set spread over hundreds of events only reconstructs if a client can pull the whole set from somewhere. No relay promises that. Whoever syncs against a relay that answers halfway computes a different list and never learns it. So keep the list computable, but let the per-follow events carry enough causal metadata to prove completeness before merging.

Replies (1)

This is completely wrong. There is no “last writer wins” in CRDTs, you’ve got to give up that CRUD mentality. Stuffing all your followings into a single “follow list” event is the problem. CRDT fixes that because it persists a history of intents. Imagine you are dealing with a monotonically increasing counter. You have two options to derive that counter: A. Emit a state event with the current count every time it increases B. Emit a increase counter event then derive the counter state by adding all the events There is not enough context with A to converge into an unambiguous state automatically, that is why clients like Nostur have to ask what you want to do when they detect something “strange” happened. It is also the reason why keeping an history of follow lists is a pointless exercise.
↑