howdy/websocket/presence
Who is on a topic right now, across every connected node.
A process tracks itself on a topic under a key, usually a user id, with a meta value describing it, such as a name or a status. One key can be tracked by several processes, as when someone has the page open in two tabs: the key stays present until the last of them leaves.
import howdy/websocket
import howdy/websocket/presence
let users = presence.new("users", encode: user_to_json, decoder: user_decoder())
websocket.new(fn(socket) {
presence.track(users, process.self(), "room:" <> room, key: user.id, meta: user)
presence.watch(users, socket, "room:" <> room)
Nil
})
A watching socket is sent the topic’s presences as soon as it watches,
then a diff of joins and leaves after every change. The browser keeps the
list with the client script served by client:
import { Presence } from "/howdy/presence.js"
const users = new Presence({ name: "users", topic: "room:lobby" })
users.onSync((list) => render(list))
socket.onmessage = (event) => {
const message = JSON.parse(event.data)
if (users.receive(message)) return
// the app's own messages
}
Server code, such as a live view, can subscribe instead and get the
same diffs as messages.
A presence lasts as long as the process that tracked it. Nothing needs
cleaning up when a socket closes or crashes. Each node runs a tracker
that owns its own processes’ presences and shares them with the trackers
on the other nodes, so list answers for the whole cluster from local
memory. When a node goes away, its presences leave on the others; if it
comes back, they rejoin. A tracker that crashes forgets its node’s
presences, as their processes are not told to track again.
Metas travel as JSON, so a node running different code can still read
them: a meta that decoder cannot read is skipped by list and
subscribe, and passed to the browser as it is.
Types
What changed on a topic. Tracking an existing process again with a new meta is a join of the new meta and a leave of the old one, so the key can appear on both sides; it is only gone once no metas remain.
pub type Diff(meta) {
Diff(
topic: String,
joins: List(Entry(meta)),
leaves: List(Entry(meta)),
)
}
Constructors
A key and the metas of the processes tracking it, oldest first.
pub type Entry(meta) {
Entry(key: String, metas: List(meta))
}
Constructors
-
Entry(key: String, metas: List(meta))
Values
pub fn client(at path: String) -> controller.Builder(Nil)
A controller serving the browser half, a JavaScript module exporting
Presence, at path, such as "/howdy/presence.js". Responses carry
an ETag, so browsers revalidate rather than download it again.
pub fn count(presence: Presence(meta), topic: String) -> Int
How many keys are on a topic. Someone with two tabs open counts once.
pub fn list(
presence: Presence(meta),
topic: String,
) -> List(Entry(meta))
Everyone on a topic, ordered by key.
pub fn new(
name: String,
encode encode: fn(meta) -> json.Json,
decoder decoder: decode.Decoder(meta),
) -> Presence(meta)
pub fn subscribe(
presence: Presence(meta),
topic: String,
subject: process.Subject(msg),
map: fn(Diff(meta)) -> msg,
) -> List(Entry(meta))
Send a message made by map to subject after every change to
topic, until the calling process exits. Returns the topic’s entries at
the moment of subscribing, so no change falls between them and the first
diff.
The diffs come from a process linked to the caller, so a crash in map
takes the caller down with it.
pub fn track(
presence: Presence(meta),
pid: process.Pid,
topic: String,
key key: String,
meta meta: meta,
) -> Nil
Track pid on topic under key until the process exits or calls
untrack. In a socket callback, pid is process.self(). Tracking the
same process, topic and key again replaces its meta, which is how a
status such as “typing” is updated.
pub fn untrack(
presence: Presence(meta),
pid: process.Pid,
topic: String,
key key: String,
) -> Nil
Stop tracking pid on topic under key. Does nothing if it was not.
pub fn watch(
presence: Presence(meta),
socket: websocket.Socket(msg),
topic: String,
) -> Nil
Keep socket’s client up to date with topic: it is sent every
presence at once, then a diff after every change, as JSON text frames
that the client script reads. Watching does not track the socket. Call
it from a socket callback.