Global/Shared Presence {:error, :nopresence}
This post talks about how to use Presence as a sharable state object that all connect users can modify and manage. In a sense, a singleton Presence object? There’s probably a better way to say that.
Problem
Only the socket/process tracking a Presence object can update it, otherwise a {:error, :nopresence}
will occur. This is by design, but we can workaround it :).
Motivation
Recently, I was working on an agile poker app written with Elixir’s Phoenix Framework that uses Presence, Channels, and LiveView. Users join a room and vote on durations for upcoming tasks in the next sprint (agile stuff). The agile poker app GitHub repo is here.
When a user joins a room they should know the current state of the room (has the game started? what stage is the game at? etc.). The easiest solution for this may be persist the state somewhere in a database, but I wanted a single-session based room where nothing is saved permanently. It occurred to me that I could use Presence to manage the overall state of the game (in addition to each individual user’s status).
- With Presence you get the benefits of channels with nice callbacks to update everything
- Presence is just really easy with minimal code
Presence is great but..
Presence is really great for tracking processes/channels for individual users, but having a “shared presence object” that everyone can update and modify is a challenge.
To update a Presence instance the docs suggest update/3 or update/4. What that often looks like is
Presence.update(
self(),
topic,
key,
state
)
The caveat is that the only the socket/PID tracking the presence object can update/modify it. Otherwise you’ll receive a {:error, :nopresence}
response back. Maybe this assumed knowledge, but it’s not mentioned in the docs. This error actually comes from Phoenix’s PubSub Tracker.Shard module.
The only post that I found mentioning with this :nopresence
issue was here.
Sharable Presence object workaround
The workaround I used was to register a process with a name so that it can be retrieved by any user.
Tracking:
Process.register(self(), :insert_name_here)
Presence.track(
self(),
topic,
key,
state
)
Update:
pid = Process.whereis(:insert_name_here)
Presence.update(
pid,
topic,
key,
state
)
Caveats
There is a limit to the number of atoms you can have in an Elixir app, thus there is a limit to the number of processes you can register. There are ways around that, ways to unregister processes, etc., but I’m not going to get into that. Mostly because I didn’t bother, but that’s something I’d monitor if I was making some more production level.
Code
Here’s the real-world implementation I used for the poker app.
Alternatives
You could probably use an Agent or better a GenServer to manage and store state, but you’ll end up having to write a bunch of code to issue callbacks and basically do all the nice things that Presence/PubSub/Channels gives you.
You could save the “game state” to each user’s Presence object. This would work, but you just have to make sure that you update all tracked users.
To do this:
- Channels/PubSub to Broadcast a message which is picked up by each connected socket
- Each socket can update its own related Presence object
For an example, look at what I did with “reset” in the agile poker code.