ORSet
A set where elements can be added and removed. When two devices edit at the same time — one adds an item, another removes it — add wins. The item stays in.
Converges to: a set containing exactly the elements whose add operations have not been causally dominated by a subsequent remove from a replica that observed that add.
Merge rule
Every add(replica, element) tags the element with a unique dot (replica, counter). A remove(element) witnesses all dots currently associated with the element in the local state and marks them as removed. On merge:
A dot present in only one replica's store (the other never saw it) is kept.
A dot present in the store of A but in the causal context of B (B saw it and removed it) is dropped.
This is why add wins over a concurrent remove: a concurrent remove only witnessed dots it already had. A new dot minted by the concurrent add was never seen by the remover, so it survives.
What each change costs to send
Ticking one item off a four-hundred-item list should cost about as much as one item, not four hundred. So add and remove hand you back the change — the element you touched, and a short note about which older versions of it this replaces — and that is what goes to the other devices. The size of what travels does not depend on how big the set is.
In code, both mutators return a Patch<ORSet<E>>. Hand it to a replicator with quilter.mutate { it.add(replica, element) }, which reads the current state under the replicator's own lock and broadcasts only the delta. If you are holding a set outside a replicator and want the resulting whole set, absorb the patch: set.piece { it.add(replica, element) }.
An add's delta names the dot it mints and the dots that add supersedes. That second part is not an optimisation detail — leave it out and a later remove retires only the dot the remover knew about, and the element comes back from the dead on every other device.
Clearing a lot of items at once
Emptying a big set is the one case where one-at-a-time hurts. Absorbing a change costs a merge across the whole set, so removing ten thousand items one by one pays ten thousand merges — seconds of work, not milliseconds. removeAll takes the whole batch and pays one.
It is otherwise the same operation: exactly the items you named go, everything else stays, and a device that reconnects with an old copy of the list does not bring the deleted items back. A change someone else made at the same time still wins, just as it does with remove.
Code example
When to use
ORSet is the right choice for most concurrent set workloads where elements can be re-added after removal. Add-wins semantics are intuitive: a concurrent re-add "undoes" a concurrent remove. For permanent removal (tombstone-wins), see TwoPhaseSet.