Application API
Chat façade
Wiregrid.Chat is the opinionated path for channel chat, direct messages, typing, reactions, edits, deletes, and read markers. It calls the same runtime as Wiregrid.publish/4. There is no second engine and no second set of sessions.
Defining the module
defmodule MyApp.Chat do
use Wiregrid.Chat,
instance: :chat,
options: [profile: :small, authorizer: MyApp.Auth],
chat: [max_message_bytes: 8_192]
end
instance is required. options is the keyword list passed to Wiregrid.Config and defaults to [], which selects the :balanced profile. chat is validated by Wiregrid.Chat.Config at compile time; unknown keys raise ArgumentError.
The generated module defines child_spec/1, so it drops into a supervision tree. It also defines instance/0, wiregrid_options/0, chat_config/0, and gateway_child_spec/1, which starts Wiregrid.Foreign.Gateway bound to the same instance. child_spec/1 is overridable.
mix wiregrid.gen.chat MyApp.Chat --instance chat --profile small writes a module of this shape.
Connect, join, say, history
{:ok, session_id} = MyApp.Chat.connect("alice")
:ok = MyApp.Chat.join(session_id, "general")
{:ok, result} = MyApp.Chat.say(session_id, "general", "hello")
{:ok, rows, next} = MyApp.Chat.history("general", nil, 100)
connect/3 is connect(user_id, pid \\ self(), opts \\ []) and returns {:ok, session_id}. join/3 subscribes to {:channel, channel}. An optional context map is stored with the subscription.
say/4 builds a %{type: :message, body: body} event and publishes it with class: :durable, persist: true, and session_id set to the speaker. Persistence uses the channel topic as the storage stream, so history/3 pages the same rows. The default page size is 100. history(channel, cursor \\ nil, limit \\ 100) returns {:ok, rows, next_cursor}.
Say options are the message fields :attachments, :mentions, :metadata, :nonce, :reply_to, plus the publish fields :event_id, :meta, :cluster, :exclude_sessions, and :exclude_users. Duplicate keys are rejected as :invalid_chat_options.
Whisper and the user stream
whisper(session_id, user_id, body, opts \\ []) sends the same message event with Wiregrid.send_user/4. The stream is {:user, user_id}. Read it back with inbox_history(user_id, cursor \\ nil, limit \\ 100). Whisper options are :event_id, :meta, and :cluster, plus the message fields. The call forces class: :durable and persist: true.
Edits, deletes, reactions, typing, reads
| Call | Event | Class |
|---|---|---|
edit(session, channel, message_id, body) | %{type: :message_edit, message_id:, body:} | durable, persisted |
delete(session, channel, message_id, reason \\ nil) | %{type: :message_delete, message_id:, reason:} | durable, persisted |
react/4 and unreact/4 | :reaction_add or :reaction_remove | durable, persisted |
mark_read(session, channel, message_id) | %{type: :read, message_id:} | ephemeral, not persisted, sender excluded |
typing(session, channel, active? \\ true) | %{type: :typing, active: boolean} | ephemeral, not persisted, sender excluded |
Bodies are binaries. Wiregrid.Chat.Event trims them and rejects a body larger than max_message_bytes with :message_too_large. An empty body with an empty attachment list is :empty_message. An empty body with attachments is allowed when allow_empty_messages_with_attachments is true, which is the default. Edits still require a non-empty body. Reactions reject an empty emoji with :empty_reaction. Message ids are binaries up to 256 bytes, or an integer or atom.
Constructors drop nil, [], and %{} fields so a plain text message stays a small map. The finished event must fit in max_chat_event_bytes.
set_presence(session_id, status, metadata \\ %{}) forwards to Wiregrid.set_presence/4. ack/1 accepts a map with session_id and delivery_id and acknowledges that one delivery. Any other term is {:error, :invalid_delivery_envelope}.
Chat limits
These defaults live in Wiregrid.Chat.Config. They are separate from the instance profile.
| Key | Default | Ceiling |
|---|---|---|
max_message_bytes | 65,536 | 1,048,576, and not above max_chat_event_bytes |
max_attachment_count | 32 | 256 |
max_mention_count | 100 | 10,000 |
max_reaction_bytes | 64 | positive integer |
max_client_nonce_bytes | 128 | positive integer |
max_chat_event_bytes | 262,144 | 1,048,576 |
message_rate_limit | {30, 10_000} | limit 1..100,000, window 100..3,600,000 ms, or nil |
activity_rate_limit | {60, 10_000} | same shape, or nil |
allow_empty_messages_with_attachments | true | boolean |
Say, whisper, edit, and delete spend the message limiter. React and typing spend the activity limiter. Both use a token bucket whose burst equals the limit, keyed by the session id under :chat_message or :chat_activity. A nil rate skips the check. Metadata maps are capped at 128 keys. Attachment and mention lists that exceed their counts return {:error, {:invalid_or_too_many, field}}.
What arrives in the mailbox
Joiners receive {:"$wiregrid", envelope}. With the default delivery_format: :term the map contains :event and the chat map above. Durable says keep a delivery reservation until MyApp.Chat.ack(envelope) or Wiregrid.ack(instance, session_id, delivery_id). Typing and read markers are ephemeral: past the soft queue they are dropped, and they do not occupy storage.