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. 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
the value type carried in each presence entry.
Properties
Functions
The causal Dots this state has delivered — (author, author-seq) per op.
The join: per-replica max-clock wins. At equal clocks, present beats null (see class-level KDoc for the tie-break rationale).