Presence
Presence, rooms, signals, typing, receipts
These features share the session and the authorizer. They do not introduce a second routing table. Ephemeral broadcasts (typing, activity) can be dropped at the soft queue. Signals published as durable room events follow the durable rules.
Presence
:ok = Wiregrid.set_presence(:chat, session_id, :online, %{})
:ok = Wiregrid.set_status(:chat, session_id, :idle)
snapshot = Wiregrid.presence(:chat, "alice")
:ok = Wiregrid.watch_presence(:chat, session_id, "bob")
:ok = Wiregrid.unwatch_presence(:chat, session_id, "bob")
Built-in statuses are :online, :idle, :dnd, :invisible, and :offline. A custom status is {:custom, binary} with 1 to 64 bytes. Anything else is {:error, :invalid_presence_status}.
presence/2 returns %{status:, sessions:, metadata:, updated_at_ms:}. With no sessions the status is :offline, sessions is 0, metadata is %{}, and updated_at_ms is 0. :offline and :invisible are omitted from the visible set before aggregation.
presence_aggregation defaults to :priority. presence_priority defaults to [:online, :dnd, :idle, :offline]. Earlier entries win. The list must be non-empty, unique, and at most 32 entries. Each entry is one of :online, :dnd, :idle, :offline, or a custom tuple. :latest selects by timestamp instead. A bad list fails startup with :invalid_presence_priority.
Watch caps: max_presence_watches_per_session is 1,024 and max_watchers_per_user is 100,000. Those two are base keys, not profile keys. The global edge table is max_presence_watch_edges, which the profile does set (50,000 / 1,000,000 / 10,000,000).
Rooms
:ok = Wiregrid.join_room(:chat, "lobby", session_id)
:ok = Wiregrid.leave_room(:chat, "lobby", session_id)
{:ok, members} = Wiregrid.room_members(:chat, "lobby", 100)
:ok = Wiregrid.set_room_metadata(:chat, "lobby", session_id, %{topic: "stand-up"})
:ok = Wiregrid.set_room_ttl(:chat, "lobby", session_id, 60_000)
A room id is a topic-shaped term checked with max_topic_bytes and max_topic_depth. Chat code that wants the tuple {:room, id} should pass that tuple; Wiregrid.Chat.room_topic/1 builds it. join_room/4 on the low-level API takes the room term and the session id, in that order.
Per-session membership is capped by max_rooms_per_session (256). A single room is capped by max_room_members (100,000). Global edges, grace entries, and room-state rows are profile limits: max_room_edges, max_room_grace, and max_room_states. Grace holds membership across reconnect_grace_ms so a resume can reclaim the seat.
room_members/3 defaults the limit to 10,000. room_metadata/2 and room_info/3 are the read side. Metadata writes and TTL changes go through the runtime and the authorizer as the acting session. Room metadata is local; a cluster does not merge maps from two nodes into one document. The latest trusted cluster update is what a node keeps.
sync_rooms/4 replaces the session’s room set. resume_room/4 moves one edge from an old session id to a new one.
Signals
:ok = Wiregrid.signal(:chat, "lobby", session_id, :offer, %{sdp: sdp})
:ok = Wiregrid.signal(:chat, "lobby", session_id, :ice_candidate, candidate, include_sender: true)
The sender must already be a member of the room. Otherwise the result is {:error, :not_room_member}. Kinds are :ring, :accept, :decline, :cancel, :offer, :answer, :ice_candidate, :leave, :reconnect, and :membership. The payload is validated as an event against max_metadata_bytes (16,384), not against max_event_bytes.
The published event is %{type: :signal, signal: kind, from_user_id:, from_session_id:, payload:}. It is sent with publish_room/4, class: :durable, cluster: true, and the sender excluded unless include_sender: true. The authorizer action is {:signal, kind}. The only option key is :include_sender.
Typing and other activity
:ok = Wiregrid.typing(:chat, session_id, {:channel, "general"})
:ok = Wiregrid.activity(:chat, session_id, {:channel, "general"}, :cursor, %{pos: 12},
ttl_ms: 5_000, broadcast: true)
{:ok, rows} = Wiregrid.activities(:chat, {:channel, "general"}, :typing, 100)
typing/4 stores kind :typing with value true and sets broadcast: true unless the caller already passed that option. Activity options are :ttl_ms and :broadcast. The default TTL is activity_ttl_ms (5,000) and cannot exceed max_ttl_ms (604,800,000, seven days). A broadcast publishes %{type: :activity, kind:, user_id:, value:} as ephemeral, excludes the sender, and asks the cluster to forward it. The authorizer action is :activity.
Per session, max_activities_per_session is 512. The global table is the profile’s max_activity_entries. activities/4 lists one topic and kind, default limit 100. Chat’s typing/3 is a different call: it publishes a %{type: :typing, active: boolean} chat event and spends the chat activity rate limit. Use one of them consistently so clients see one shape.
Receipts
:ok = Wiregrid.receipt(:chat, session_id, %{id: "r1", message_id: "m1", status: "read"})
{:ok, receipt} = Wiregrid.get_receipt(:chat, session_id, "r1")
A receipt is a map with an id. Options are :ttl_ms and :persist. The default TTL is receipt_ttl_ms (86,400,000, one day). persist defaults to false; when true, the receipt is appended on the user stream {:user, user_id}. The authorizer action is :receipt. Caps are max_receipts_per_session (2,048) and the profile’s max_receipt_entries. Chat’s mark_read/3 does not write this table. It publishes an ephemeral %{type: :read} event on the channel.