-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
presence.ex
251 lines (206 loc) · 8.9 KB
/
presence.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
defmodule Phoenix.Presence do
@moduledoc """
Provides Presence tracking to processes and channels.
This behaviour provides presence features such as fetching
presences for a given topic, as well as handling diffs of
join and leave events as they occur in real-time. Using this
module defines a supervisor and allows the calling module to
implement the `Phoenix.Tracker` behaviour which starts a
tracker process to handle presence information.
## Example Usage
Start by defining a presence module within your application
which uses `Phoenix.Presence` and provide the `:otp_app` which
holds your configuration, as well as the `:pubsub_server`.
defmodule MyApp.Presence do
use Phoenix.Presence, otp_app: :my_app,
pubsub_server: MyApp.PubSub
end
The `:pubsub_server` must point to an existing pubsub server
running in your application, which is included by default as
`MyApp.PubSub` for new applications.
Next, add the new supervisor to your supervision tree in `lib/my_app.ex`:
children = [
...
supervisor(MyApp.Presence, []),
]
Once added, presences can be tracked in your channel after joining:
defmodule MyApp.MyChannel do
use MyApp.Web, :channel
alias MyApp.Presence
def join("some:topic", _params, socket) do
send(self, :after_join)
{:ok, assign(socket, :user_id, ...)}
end
def handle_info(:after_join, socket) do
push socket, "presence_state", Presence.list(socket)
{:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
online_at: inspect(System.system_time(:seconds))
})
{:noreply, socket}
end
end
In the example above, `Presence.track` is used to register this
channel's process as a presence for the socket's user ID, with
a map of metadata. Next, the current presence information for
the socket's topic is pushed to the client as a `"presence_state"` event.
Finally, a diff of presence join and leave events will be sent to the
client as they happen in real-time with the "presence_diff" event.
The diff structure will be a map of `:joins` and `:leaves` of the form:
%{joins: %{"123" => %{metas: [%{status: "away", phx_ref: ...}]},
leaves: %{"456" => %{metas: [%{status: "online", phx_ref: ...}]},
See `Phoenix.Presence.list/2` for more information on the presence
datastructure.
## Fetching Presence Information
Presence metadata should be minimized and used to store small,
ephemeral state, such as a user's "online" or "away" status.
More detailed information, such as user details that need to
be fetched from the database can be achieved overriding the `fetch/2`
function. The `fetch/2` callback is triggered when using `list/1`
and serves as a mechanism to fetch presence information a single time,
before broadcasting the information to all channel subscribers.
This prevents N query problems and gives you a single place to group
isolated data fetching to extend presence metadata. The function must
return a map of data matching the outlined Presence datastructure,
including the `:metas` key, but can extend the map of information
to include any additional information. For example:
def fetch(_topic, entries) do
query =
from u in User,
where: u.id in ^Map.keys(entries),
select: {u.id, u}
users = query |> Repo.all |> Enum.into(%{})
for {key, %{metas: metas}} <- entries, into: %{} do
{key, %{metas: metas, user: users[key]}}
end
end
The function above fetches all users from the database who
have registered presences for the given topic. The fetched
information is then extended with a `:user` key of the user's
information, while maintaining the required `:metas` field from the
original presence data.
"""
alias Phoenix.Socket.Broadcast
@type presences :: %{ String.t => %{metas: [map]}}
@type presence :: %{key: String.t, meta: map}
@type topic :: String.t
@callback start_link(Keyword.t) :: {:ok, pid} | {:error, reason :: term} :: :ignore
@callback init(Keyword.t) :: {:ok, pid} | {:error, reason :: term}
@callback track(Phoenix.Socket.t, key :: String.t, meta :: map) :: :ok
@callback track(pid, topic, key :: String.t, meta ::map) :: :ok
@callback untrack(Phoenix.Socket.t, key :: String.t) :: :ok
@callback untrack(pid, topic, key :: String.t) :: :ok
@callback update(Phoenix.Socket.t, key :: String.t, meta :: map) :: :ok
@callback update(pid, topic, key :: String.t, meta ::map) :: :ok
@callback fetch(topic, presences) :: presences
@callback list(topic) :: presences
@callback handle_diff(%{topic => {joins :: presences, leaves :: presences}}, state :: term) :: {:ok, state :: term}
defmacro __using__(opts) do
quote do
@opts unquote(opts)
@otp_app @opts[:otp_app] || raise "presence expects :otp_app to be given"
@behaviour unquote(__MODULE__)
@task_supervisor Module.concat(__MODULE__, TaskSupervisor)
def start_link(opts \\ []) do
opts = Keyword.merge(@opts, opts)
Phoenix.Presence.start_link(__MODULE__, @otp_app, @task_supervisor, opts)
end
def init(opts) do
server = Keyword.fetch!(opts, :pubsub_server)
{:ok, %{pubsub_server: server,
node_name: Phoenix.PubSub.node_name(server),
task_sup: @task_supervisor}}
end
def track(%Phoenix.Socket{} = socket, key, meta) do
track(socket.channel_pid, socket.topic, key, meta)
end
def track(pid, topic, key, meta) do
Phoenix.Tracker.track(__MODULE__, pid, topic, key, meta)
end
def untrack(%Phoenix.Socket{} = socket, key) do
untrack(socket.channel_pid, socket.topic, key)
end
def untrack(pid, topic, key) do
Phoenix.Tracker.untrack(__MODULE__, pid, topic, key)
end
def update(%Phoenix.Socket{} = socket, key, meta) do
update(socket.channel_pid, socket.topic, key, meta)
end
def update(pid, topic, key, meta) do
Phoenix.Tracker.update(__MODULE__, pid, topic, key, meta)
end
def fetch(_topic, presences), do: presences
def list(%Phoenix.Socket{topic: topic}), do: list(topic)
def list(topic) do
Phoenix.Presence.list(__MODULE__, topic)
end
def handle_diff(diff, state) do
Phoenix.Presence.handle_diff(__MODULE__,
diff, state.node_name, state.pubsub_server, state.task_sup
)
{:ok, state}
end
defoverridable fetch: 2
end
end
@doc false
def start_link(module, otp_app, task_supervisor, opts) do
import Supervisor.Spec
opts =
opts
|> Keyword.merge(Application.get_env(otp_app, module) || [])
|> Keyword.put(:name, module)
children = [
supervisor(Task.Supervisor, [[name: task_supervisor]]),
worker(Phoenix.Tracker, [module, opts, opts])
]
Supervisor.start_link(children, strategy: :one_for_one)
end
@doc false
def handle_diff(module, diff, node_name, pubsub_server, sup_name) do
Task.Supervisor.start_child(sup_name, fn ->
for {topic, {joins, leaves}} <- diff do
msg = %Broadcast{topic: topic, event: "presence_diff", payload: %{
joins: module.fetch(topic, group(joins)),
leaves: module.fetch(topic, group(leaves))
}}
Phoenix.PubSub.direct_broadcast!(node_name, pubsub_server, topic, msg)
end
end)
end
@doc """
Returns presences for a topic.
## Presence datastructure
The presence information is returned as map with presences grouped
by key, cast as a string, and accumulated metadata, with the following form:
%{key => %{metas: [%{phx_ref: ..., ...}, ...]}}
For example, imagine a user with id `123` online from two
different devices, as well as a user with id `456` online from
just one device. The following presence information might be returned:
%{"123" => %{metas: [%{status: "away", phx_ref: ...},
%{status: "online", phx_ref: ...}]},
"456" => %{metas: [%{status: "online", phx_ref: ...}]}}
The keys of the map will usually point to a resource ID. The value
will contain a map with a `:metas` key containing a list of metadata
for each resource. Additionally, every metadata entry will contain a
`:phx_ref` key which can be used to uniquely identify metadata for a
given key. In the event that the metadata was previously updated,
a `:phx_ref_prev` key will be present containing the previous
`:phx_ref` value.
"""
def list(module, topic) do
grouped =
module
|> Phoenix.Tracker.list(topic)
|> group()
module.fetch(topic, grouped)
end
defp group(presences) do
presences
|> Enum.reverse()
|> Enum.reduce(%{}, fn {key, meta}, acc ->
Map.update(acc, to_string(key), %{metas: [meta]}, fn %{metas: metas} ->
%{metas: [meta | metas]}
end)
end)
end
end