EphemeralMap

@Serializable
class EphemeralMap<V> : Quilted<EphemeralMap<V>>

A presence/awareness CRDT.

What this models

Each replica (A, B, C, …) owns exactly one slot. A peer can write an arbitrary value V into its own slot ("I am present with cursor = X") or explicitly vacate it ("I am leaving"). Entries expire on observers that have not received a heartbeat within a caller-supplied TTL.

Design decisions

Expiry clock — local receive time. Cross-peer wall-clock comparison is unbounded under clock skew (wasmJs, iOS). Instead, every observer measures staleness by its own locally-stamped receive time: when was the last update from replica R received here? The CRDT carries a per-replica monotonic EphemeralEntry.clock for ordering re-publishes, but that clock is never compared across replica slots. Make the time source injectable (see EphemeralMapTracker); the CRDT itself is time-free.

Graceful departure — null + higher clock. Yjs Awareness pattern. leave writes a null-valued entry with a clock one higher than the current. Peers that merge the departure suppress the slot from live output even if a stale presence entry with a lower clock also exists.

Tie-break at equal clocks — present beats null. A crash-detector tombstone minted at seenClock + 1 can collide with a live peer's next heartbeat if both increment from the same base. At equal clock, piece keeps the non-null (present) entry, so a live peer's heartbeat is never evicted by a same-clock departure. Null-vs-null at equal clock is a no-op. Value-vs-value at equal clock for the same replica is precluded by the single-writer contract (each replica writes only its own slot), so no second tie-break is needed there.

TTL eviction location. The CRDT state is time-free and serialisable: it holds all entries, including stale and null ones. The live helper filters entries given a caller-supplied receive-time map and a now timestamp — it is pure and does not mutate any state. EphemeralMapTracker wraps the CRDT with an injectable clock, maintains the receive-time map, and surfaces a single live() call that drives eviction.

Each replica writes only its own slot. There is no mechanism for replica A to write into B's slot, so no tombstone or add-wins logic is needed — absence after TTL is sufficient for removal.

Not durable. This CRDT is intentionally not designed for persistence across reconnect. Use LWWMap or ORMap for durable key→value mappings.

Reconnect and clock-reset recovery

When a replica restarts it resets its local clock to zero (or a low value), which is below the stale high-clock entry that peers already have for that replica. Writes from the restarted replica will be silently dropped by piece and put until its clock catches up. The only recovery mechanism is TTL eviction: each observer holds the stale entry until it expires, at which point the slot is cleared and the next heartbeat from the restarted replica is accepted as fresh. Rejoin-visibility latency is therefore bounded by ttlMs. There is no explicit "reset" message — design reconnect flows to either (a) persist and restore the last clock so it is always increasing, or (b) accept the TTL-bounded window before the restarted replica becomes visible.

Type Parameters

V

the value type carried in each presence entry.

Types

Link copied to clipboard
object Companion

Properties

Link copied to clipboard

Per-replica latest entry. Null value = departed; null key = never heard of.

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
open operator override fun equals(other: Any?): Boolean
Link copied to clipboard
open override fun hashCode(): Int
Link copied to clipboard
fun leave(replica: ReplicaId, clock: Long): EphemeralMap<V>

Signal graceful departure for replica: publishes a null-value entry with clock, which must be higher than any prior entry. Peers that merge this departure will suppress replica from live output.

Link copied to clipboard
fun live(receiveTime: Map<ReplicaId, Long>, now: Long, ttlMs: Long): Map<ReplicaId, V>

Returns the set of live entries: those with a non-null value whose receive time is within ttlMs milliseconds of now.

Link copied to clipboard
open override fun piece(other: EphemeralMap<V>): EphemeralMap<V>

The join: per-replica max-clock wins. At equal clocks, present beats null (see class-level KDoc for the tie-break rationale).

Link copied to clipboard
fun put(replica: ReplicaId, value: V, clock: Long): EphemeralMap<V>

Publish or update this replica's presence with value and the given clock.

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