March Docs

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

typePresenceMetaPresenceMeta = PresenceMeta(Int, String)#

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})

typePresenceEntryPresenceEntry = PresenceEntry(List(PresenceMeta))#

All presence entries for one user key on a topic. Holds a list of PresenceMeta (one per active device/connection).

typePresenceStatePresenceState = PresenceState(Map(String, Map(String, PresenceEntry)))#

Immutable presence state: Map(topic, Map(key, PresenceEntry)). Both levels use String keys with the standard string comparator.

Functions

fnconnection_countconnection_count(pst, topic, key)#

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")
0
fncount_presencescount_presences(pst, topic)#

Number 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")
0
fnentry_countentry_count(e)#

Number of connections (metas) in this entry. O(n).

Presence.entry_count(Presence.make_entry([Presence.make_meta(1, "{}"), Presence.make_meta(2, "{}")]))
-- 2
fnentry_metasentry_metas(e)#

The metas of an entry; track_state puts the newest connection first.

Examples

march> Presence.entry_metas(Presence.make_entry([]))
[]
fnis_presentis_present(pst, topic, key)#

True when key has at least one active presence on topic.

Examples

march> Presence.is_present(Presence.new_state(), "room:lobby", "alice")
false
fnjoin_diff_jsonjoin_diff_json(key, meta_json)#

Build 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\":{}}"
fnleave_diff_jsonleave_diff_json(key, meta_json)#

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\":[{}]}}}"
fnlist_statelist_state(pst, topic)#

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"))
0
fnmake_entrymake_entry(metas)#

A PresenceEntry holding metas (one per connection).

Examples

march> Presence.entry_metas(Presence.make_entry([Presence.make_meta(1, "{}")]))
[PresenceMeta(1, "{}")]
fnmake_metamake_meta(pid, data_json)#

One connection's presence: the channel pid and its JSON metadata.

Examples

march> Presence.make_meta(7, "{}")
PresenceMeta(7, "{}")
fnmeta_datameta_data(m)#

The JSON metadata string of a PresenceMeta.

Examples

march> Presence.meta_data(Presence.make_meta(7, "{\"device\":\"phone\"}"))
"{\"device\":\"phone\"}"
fnmeta_pidmeta_pid(m)#

The channel pid of a PresenceMeta.

Examples

march> Presence.meta_pid(Presence.make_meta(7, "{}"))
7
fnnew_statenew_state()#

An empty PresenceState: nobody is present on any topic.

Examples

march> Presence.count_presences(Presence.new_state(), "room:lobby")
0
fntracktrack(sock, key, meta_json)#

Track 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\"}")
fntrack_statetrack_state(pst, topic, key, pid, meta_json)#

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")   -- 2
fnuntrackuntrack(sock, key, meta_json)#

Untrack 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\"}")
fnuntrack_stateuntrack_state(pst, topic, key, pid)#

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")   -- false

Examples

march> Presence.count_presences(Presence.untrack_state(Presence.new_state(), "room:lobby", "alice", 7), "room:lobby")
0