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 survived

Types

Link copied to clipboard
object Companion

Properties

Link copied to clipboard
val value: Long

The counter's current value: sum of all live increment amounts.

Functions

Link copied to clipboard
open fun causalDots(): Set<Dot>

The causal Dots this state has delivered — (author, author-seq) per op.

Link copied to clipboard

The per-author high-water of dots this state delivered and has since compacted away without retaining their identities.

Link copied to clipboard
open operator override fun equals(other: Any?): Boolean
Link copied to clipboard
open override fun hashCode(): Int
Link copied to clipboard

Increment by by (must be positive) on behalf of replica. Returns the delta to absorb with piece; the receiver is unchanged.

Link copied to clipboard
open override fun piece(other: ResettableCounter): ResettableCounter

The causal merge of two replicas.

Link copied to clipboard

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.

Link copied to clipboard
open override fun toString(): String