Class PresenceRegistry

java.lang.Object
su.onno.ui.presence.PresenceRegistry

public class PresenceRegistry extends Object
In-memory registry of who is currently viewing each record, for record-level collaboration markers ("3 people are here", like the avatars in a shared document).

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:

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 Details

    • DEFAULT_TTL_MILLIS

      public static final long DEFAULT_TTL_MILLIS
      Default 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_MILLIS
      Default interval between expiry sweeps.
      See Also:
  • Constructor Details

  • 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

      public void onRemote(ClusterEvent.Presence presence)
      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

      public List<Map<String,String>> viewers(String kind, String entityName, String id)
      The current viewers of a record as {userId, displayName} maps — what the enter response returns.
    • allViewers

      public List<Map<String,Object>> 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.