Packages
phoenix_kit
1.7.39
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/entities/presence_helpers.ex
defmodule PhoenixKit.Modules.Entities.PresenceHelpers do
@moduledoc """
Helper functions for collaborative editing with Phoenix.Presence.
Provides utilities for tracking editing sessions, determining owner/spectator roles,
and syncing state between users.
"""
alias PhoenixKit.Modules.Entities.Presence
@doc """
Tracks the current LiveView process in a Presence topic.
## Parameters
- `type`: The resource type (`:entity` or `:data`)
- `id`: The resource ID
- `socket`: The LiveView socket
- `user`: The current user struct
## Examples
track_editing_session(:entity, 5, socket, user)
# => {:ok, ref}
"""
def track_editing_session(type, id, socket, user) do
topic = editing_topic(type, id)
Presence.track(self(), topic, socket.id, %{
user_id: user.id,
user_email: user.email,
user: user,
joined_at: System.system_time(:millisecond),
phx_ref: socket.id,
# For diagnostics and dead process detection
pid: self(),
transport_pid: socket.transport_pid
})
end
@doc """
Determines if the current socket is the owner (first in the presence list).
Returns `{:owner, presences}` if this socket is the owner (or same user in different tab), or
`{:spectator, owner_meta, presences}` if a different user is the owner.
## Examples
case get_editing_role(:entity, 5, socket.id, current_user.id) do
{:owner, all_presences} ->
# I can edit!
{:spectator, owner_metadata, all_presences} ->
# I'm read-only, sync with owner's state
end
"""
def get_editing_role(type, id, socket_id, current_user_id) do
presences = get_sorted_presences(type, id)
case presences do
[] ->
# No one here (shouldn't happen since caller is here)
# But treat as owner to avoid blocking
{:owner, []}
[{^socket_id, _meta} | _rest] ->
# I'm first! I'm the owner
{:owner, presences}
[{_other_socket_id, owner_meta} | _rest] ->
# Check if same user (different tab) or different user
if owner_meta.user_id == current_user_id do
# Same user, different tab - treat as owner so both tabs can edit
{:owner, presences}
else
# Different user - spectator mode (FIFO locking)
{:spectator, owner_meta, presences}
end
end
end
@doc """
Gets all presences for a resource, sorted by join time (FIFO).
Returns a list of tuples: `[{socket_id, metadata}, ...]`
## Examples
get_sorted_presences(:entity, 5)
# => [
# {"phx-abc123", %{user_id: 5, joined_at: 123456, ...}},
# {"phx-def456", %{user_id: 7, joined_at: 123458, ...}}
# ]
"""
def get_sorted_presences(type, id) do
topic = editing_topic(type, id)
raw_presences = Presence.list(topic)
raw_presences
|> Enum.flat_map(fn {socket_id, %{metas: metas}} ->
# Filter out metas with dead PIDs
valid_metas =
Enum.filter(metas, fn meta ->
case Map.get(meta, :pid) do
pid when is_pid(pid) -> Process.alive?(pid)
# Keep metas without PID for backward compatibility
_ -> true
end
end)
# Take the first valid meta (most recent)
case valid_metas do
[meta | _] -> [{socket_id, meta}]
[] -> []
end
end)
|> Enum.sort_by(fn {_socket_id, meta} -> meta.joined_at end)
end
@doc """
Gets the lock owner's metadata, or nil if no one is editing.
## Examples
case get_lock_owner(:entity, 5) do
nil -> # No one editing
meta -> # meta.user, meta.joined_at, etc.
end
"""
def get_lock_owner(type, id) do
case get_sorted_presences(type, id) do
[{_socket_id, meta} | _] -> meta
[] -> nil
end
end
@doc """
Gets all spectators (everyone except the first person).
Returns a list of metadata for spectators only.
## Examples
get_spectators(:entity, 5)
# => [
# %{user_id: 7, user_email: "user@example.com", joined_at: 123458, ...},
# %{user_id: 9, user_email: "other@example.com", joined_at: 123460, ...}
# ]
"""
def get_spectators(type, id) do
case get_sorted_presences(type, id) do
[] -> []
[_owner | spectators] -> Enum.map(spectators, fn {_id, meta} -> meta end)
end
end
@doc """
Counts total number of people editing (owner + spectators).
"""
def count_editors(type, id) do
get_sorted_presences(type, id) |> length()
end
@doc """
Subscribes the current process to presence events for a resource.
After subscribing, the process will receive:
- `%Phoenix.Socket.Broadcast{event: "presence_diff", ...}` when users join/leave
## Examples
subscribe_to_editing(:entity, 5)
# Now will receive presence_diff messages
"""
def subscribe_to_editing(type, id) do
topic = editing_topic(type, id)
Phoenix.PubSub.subscribe(:phoenix_kit_internal_pubsub, topic)
end
@doc """
Generates the Presence topic name for a resource.
## Examples
editing_topic(:entity, 5)
# => "entity_edit:5"
editing_topic(:data, 10)
# => "data_edit:10"
"""
def editing_topic(:entity, id), do: "entity_edit:#{id}"
def editing_topic(:data, id), do: "data_edit:#{id}"
end