ResettableCounter
A counter that supports reset-to-zero without coordination, using causal context to distinguish increments the resetter has observed (cleared by a reset) from those concurrent with it (which survive the merge).
This is the counter analogue of observed-remove: a reset removes exactly the increments it has causally witnessed, so an increment that raced with the reset — minted on a replica that had not yet seen the reset — survives and appears in the merged value.
Convergence rule: Causal<DotFun<Long>>. Each increment mints a fresh Dot carrying the by amount. A reset retires every currently-live dot into the causal context and empties the store. Subsequent increments carry dots the resetter has not yet witnessed, so they survive the causal merge.
Use cases: per-round scores, seasonal leaderboards, or any counter that needs a clean-slate without coordination.
Delta-state: increment and reset return a Patch that any replica absorbs with piece. The receiver is never mutated.
Serialization: the internal dot map uses Dot as a key. Standard JSON requires Json { allowStructuredMapKeys = true }; CBOR and Protobuf encode it cleanly without any flag.
Causal invariant: every dot in causal.store is already witnessed by causal.context. This is established by increment (which calls causal.context.add(dot) when minting a new dot) and preserved by piece (Causal.piece unions both contexts, so new store dots from either replica are always witnessed). reset exploits this invariant: it needs the current context unchanged and an empty store, so no fold over live dots is required — the context is already complete.
Samples
val a = ReplicaId("A")
val b = ReplicaId("B")
// Shared start: A has incremented 10.
var shared = ResettableCounter.ZERO
shared = shared.piece(shared.increment(a, 10L))
// B resets based on what it observed (the 10 from A).
val afterReset = shared.piece(shared.reset())
// Concurrently, A increments 3 more — A hasn't seen B's reset yet.
val concurrentAdd = shared.piece(shared.increment(a, 3L))
// Merge: the pre-reset 10 is gone; the concurrent 3 survives.
val merged = afterReset.piece(concurrentAdd)
check(merged.value == 3L) // only the concurrent increment survivedProperties
Functions
The causal Dots this state has delivered — (author, author-seq) per op.
The per-author high-water of dots this state delivered and has since compacted away without retaining their identities.
The causal merge of two replicas.
Reset the counter to zero: clear the store and carry the current causal context forward. Returns the delta to absorb with piece; the receiver is unchanged.