EphemeralMap
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. The join (piece, put) alone would silently drop the restarted replica's writes until its clock catches up. TTL eviction is what recovers it: an observer measures staleness by its own local receive time, and once a slot has gone ttlMs without a fresh update it reads as absent (EphemeralMapTracker evicts it on the next inbound update — see evicting), so the restarted replica's next heartbeat is accepted as fresh even though its clock counter is lower. Rejoin-visibility latency is therefore bounded by ttlMs from the restart's first heartbeat — not unbounded. Note this is a per-observer, receive-time signal: within the TTL window an observer that has not yet expired the dead slot still hides the restart (the accepted ephemeral within-TTL skew), and a stale re-delivery of the dead incarnation's high-clock entry (e.g. via anti-entropy) can re-pin it.
TTL eviction also cannot recover a replica that departed gracefully: leave is permanent by design, and the tracker will not drop a tombstone to admit an entry that tombstone already outranks (#1675). Such a replica rejoins only by out-clocking its own departure.
So for clock-domination that does not depend on TTL timing — and for any rejoin after leave — drive put from a clock whose high bits carry a per-boot incarnation epoch, so a restart's clock always exceeds the dead incarnation's; then TTL eviction is only the backstop, not the sole mechanism. IncarnationClock packs exactly that layout.
Type Parameters
the value type carried in each presence entry.
Properties
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 join: per-replica max-clock wins. At equal clocks, present beats null (see class-level KDoc for the tie-break rationale).