Presence
Presence module: per-topic user presence tracking for Channels.
Presence tracks which users are connected to a topic, with metadata. Key features: Multi-device: one user (key) can have multiple connections (metas). Diffs: when a user joins or leaves a topic, a "presence_diff" broadcast is sent. Clients maintain a local mirror and apply diffs. Pure layer: PresenceState is an immutable data structure fully testable without the actor runtime.
Data model: PresenceState = Map(topic, Map(key, List(PresenceMeta))) key — identifies the user (e.g. user id string) meta — arbitrary JSON string with per-device metadata pid — channel process pid (used to distinguish multi-device entries)
Pure API: new_state/0 — empty PresenceState track_state/5 — add a (pid, meta) entry for (topic, key) untrack_state/4 — remove entries for (topic, key, pid) list_state/2 — get Map(key, PresenceEntry) for a topic entry_metas/1 — get the List(PresenceMeta) from an entry meta_pid/1 — extract pid from a PresenceMeta meta_data/1 — extract data string from a PresenceMeta count_presences/2 — number of distinct keys present on a topic is_present/3, connection_count/3 join_diff_json/2, leave_diff_json/2 — presence_diff payloads
Runtime helpers (require actor runtime): track/3 — track(socket, key, meta_json): broadcast a join diff untrack/3 — untrack(socket, key, meta_json): broadcast a leave diff Neither updates a PresenceState; the caller keeps that with track_state/untrack_state.
Types
One presence entry for a single connection. pid — the channel process pid for this connection data — JSON-encoded metadata string (e.g. {"name":"Alice","online_at":0})
All presence entries for one user key on a topic. Holds a list of PresenceMeta (one per active device/connection).
Immutable presence state: Map(topic, Map(key, PresenceEntry)). Both levels use String keys with the standard string comparator.
Functions
Number of active connections (metas) for key on topic. Useful for multi-device detection (> 1 means multiple devices). 0 when the key is not present.
Examples
march> Presence.connection_count(Presence.new_state(), "room:lobby", "alice")
0Number of distinct user keys currently present on topic (a user on several devices counts once).
Examples
march> Presence.count_presences(Presence.new_state(), "room:lobby")
0Number of connections (metas) in this entry. O(n).
Presence.entry_count(Presence.make_entry([Presence.make_meta(1, "{}"), Presence.make_meta(2, "{}")]))
-- 2The metas of an entry; track_state puts the newest connection first.
Examples
march> Presence.entry_metas(Presence.make_entry([]))
[]True when key has at least one active presence on topic.
Examples
march> Presence.is_present(Presence.new_state(), "room:lobby", "alice")
falseBuild a JSON presence-diff string for a single join event. Format: {"joins":{"key":{"metas":[meta_json]}},"leaves":{}} The key is escaped for " and backslash; meta_json is embedded verbatim.
Examples
march> Presence.join_diff_json("alice", "{}")
"{\"joins\":{\"alice\":{\"metas\":[{}]}},\"leaves\":{}}"Build a JSON presence-diff string for a single leave event. Format: {"joins":{},"leaves":{"key":{"metas":[meta_json]}}} The key is escaped for " and backslash; meta_json is embedded verbatim.
Examples
march> Presence.leave_diff_json("alice", "{}")
"{\"joins\":{},\"leaves\":{\"alice\":{\"metas\":[{}]}}}"Return the Map(key, PresenceEntry) for topic. Empty map if no one is present. A topic whose last key was untracked stays in the state with an empty map.
Examples
march> Map.size(Presence.list_state(Presence.new_state(), "room:lobby"))
0A PresenceEntry holding metas (one per connection).
Examples
march> Presence.entry_metas(Presence.make_entry([Presence.make_meta(1, "{}")]))
[PresenceMeta(1, "{}")]One connection's presence: the channel pid and its JSON metadata.
Examples
march> Presence.make_meta(7, "{}")
PresenceMeta(7, "{}")The JSON metadata string of a PresenceMeta.
Examples
march> Presence.meta_data(Presence.make_meta(7, "{\"device\":\"phone\"}"))
"{\"device\":\"phone\"}"The channel pid of a PresenceMeta.
Examples
march> Presence.meta_pid(Presence.make_meta(7, "{}"))
7An empty PresenceState: nobody is present on any topic.
Examples
march> Presence.count_presences(Presence.new_state(), "room:lobby")
0Track the current user on the socket's topic: casts a ChannelBroadcast(topic, "presence_diff", join_diff_json(key, meta_json)) to the socket's channel pid. It does not touch any PresenceState; keep one with track_state. Requires actor runtime and a running PubSub.
Presence.track(sock, "alice", "{\"device\":\"phone\"}")Add a presence entry for key on topic with the given channel pid and meta_json metadata string. Returns the updated state. Multiple calls with different pids add multiple entries (multi-device support). The new meta is prepended to the key's list. Tracking the same pid twice adds a second meta for it (both are removed by one untrack_state).
let s = Presence.track_state(Presence.new_state(), "room:lobby", "alice", 7, "{}")
let s2 = Presence.track_state(s, "room:lobby", "alice", 8, "{}")
Presence.count_presences(s2, "room:lobby") -- 1
Presence.connection_count(s2, "room:lobby", "alice") -- 2Untrack the current user from the socket's topic: casts a ChannelBroadcast(topic, "presence_diff", leave_diff_json(key, meta_json)) to the socket's channel pid. It ALWAYS broadcasts the leave: it does not check whether this was the user's last connection, and it does not touch any PresenceState (use untrack_state and connection_count for that). Requires actor runtime.
Presence.untrack(sock, "alice", "{\"device\":\"phone\"}")Remove all presence metas for pid under key on topic. If the entry becomes empty, the key is removed from the topic map. Returns the updated state; unchanged when the key is not present (an unknown pid under a present key re-stores the same entry).
let s = Presence.track_state(Presence.new_state(), "room:lobby", "alice", 7, "{}")
Presence.is_present(Presence.untrack_state(s, "room:lobby", "alice", 7), "room:lobby", "alice") -- falseExamples
march> Presence.count_presences(Presence.untrack_state(Presence.new_state(), "room:lobby", "alice", 7), "room:lobby")
0