Class PresenceRegistry
State is per-node and ephemeral: a (entityType, entityName, id) record maps to its set of
viewers, each carrying a lastSeen stamped from this node's clock. A viewer is kept
alive by heartbeats and expires by TTL when they stop arriving — so a closed tab or a crashed peer
self-heals without an explicit leave, and no cross-node clock agreement is needed.
Two inputs feed the same map:
- Local — a browser on this node calls the presence endpoint (
onLocal(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)); the change is applied, broadcast to peer nodes over theClusterEventBus, and pushed to this node's SSE clients. - Remote — a peer relayed a
ClusterEvent.Presenceover the bus (onRemote(su.onno.cluster.ClusterEvent.Presence)); it is applied and pushed to this node's SSE clients, but never re-broadcast (no relay loop).
An SSE snapshot is pushed only when a record's viewer set changes (a join or a leave), not on every heartbeat, so liveness traffic stays off the browser stream. Like everything on the cluster bus this is best-effort: a dropped ping costs at most one TTL of staleness.
All per-record mutations run inside the outer ConcurrentHashMap.compute(K, java.util.function.BiFunction<? super K, ? super V, ? extends V>) callback, so the inner
plain maps are only ever touched while the bin lock is held — no separate synchronization is required,
and an emptied record is dropped atomically without the classic remove-empty-inner-map race.
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final longDefault interval between expiry sweeps.static final longDefault viewer time-to-live: a viewer with no heartbeat for this long is dropped. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionA snapshot of every record with viewers, as{kind, name, id, viewers}maps — the initial picture the ambient-presence store loads before live SSE deltas keep it current.voidonLocal(String action, String kind, String entityName, String id, String userId, String displayName, String avatarUrl) A browser on this node entered/refreshed/left a record.voidonRemote(ClusterEvent.Presence presence) A peer node relayed a presence change.voidstart()voidstop()voidRemove viewers whose last heartbeat predates the TTL, pushing a fresh snapshot for each affected record.The current viewers of a record as{userId, displayName}maps — what the enter response returns.
-
Field Details
-
DEFAULT_TTL_MILLIS
public static final long DEFAULT_TTL_MILLISDefault viewer time-to-live: a viewer with no heartbeat for this long is dropped. ~3 missed beats.- See Also:
-
DEFAULT_SWEEP_INTERVAL_MILLIS
public static final long DEFAULT_SWEEP_INTERVAL_MILLISDefault interval between expiry sweeps.- See Also:
-
-
Constructor Details
-
PresenceRegistry
-
-
Method Details
-
start
@PostConstruct public void start() -
stop
@PreDestroy public void stop() -
onLocal
public void onLocal(String action, String kind, String entityName, String id, String userId, String displayName, String avatarUrl) A browser on this node entered/refreshed/left a record. Apply, relay to peers, fan out locally. -
onRemote
A peer node relayed a presence change. Apply and fan out locally; never re-broadcast. -
sweepExpired
public void sweepExpired()Remove viewers whose last heartbeat predates the TTL, pushing a fresh snapshot for each affected record. -
viewers
The current viewers of a record as{userId, displayName}maps — what the enter response returns. -
allViewers
A snapshot of every record with viewers, as{kind, name, id, viewers}maps — the initial picture the ambient-presence store loads before live SSE deltas keep it current. Built under each record's bin lock, so it is internally consistent per record.
-