Packages

phoenix_kit

1.7.28
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
phoenix_kit lib modules sync session_store.ex
Raw

lib/modules/sync/session_store.ex

defmodule PhoenixKit.Modules.Sync.SessionStore do
@moduledoc """
ETS-based session storage for DB Sync module.
Stores sync sessions in ETS for fast, ephemeral access.
Sessions remain valid as long as the owning LiveView process is alive.
When the LiveView terminates (page closed), the session is automatically deleted.
## Architecture
This module uses a GenServer to manage an ETS table. The ETS table
provides fast reads while the GenServer handles process monitoring
and automatic cleanup when LiveView processes terminate.
## Future Migration Path
This module is designed to be easily replaced with database persistence
if audit logging or sync history is needed. The public API would remain
the same, only the storage backend would change.
## Session Structure
%{
code: "A7X9K2M4",
direction: :send | :receive,
status: :pending | :connected | :completed | :failed,
owner_pid: #PID<0.123.0>, # Session is deleted when this process dies
created_at: ~U[2025-12-16 12:15:00Z],
connected_at: nil | ~U[...],
sender_info: nil | %{...},
receiver_info: nil | %{...}
}
"""
use GenServer
require Logger
@table_name :phoenix_kit_sync_sessions
@monitors_table :phoenix_kit_sync_monitors
@cleanup_interval :timer.hours(1)
# ===========================================
# PUBLIC API
# ===========================================
@doc """
Starts the SessionStore GenServer.
"""
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Creates a new session in the store and monitors the owner process.
## Parameters
- `session` - Map with at least `code` and `owner_pid` fields
## Returns
- `:ok` on success
- `{:error, :already_exists}` if code already exists
"""
@spec create(map()) :: :ok | {:error, :already_exists}
def create(%{code: code, owner_pid: pid} = session) when is_binary(code) and is_pid(pid) do
case :ets.insert_new(@table_name, {code, session}) do
true ->
# Ask GenServer to monitor the owner process
GenServer.cast(__MODULE__, {:monitor_session, code, pid})
:ok
false ->
{:error, :already_exists}
end
end
def create(%{code: code} = session) when is_binary(code) do
# Fallback for sessions without owner_pid (backwards compatibility)
case :ets.insert_new(@table_name, {code, session}) do
true -> :ok
false -> {:error, :already_exists}
end
end
@doc """
Gets a session by its code.
## Returns
- `{:ok, session}` if found
- `{:error, :not_found}` if not found
"""
@spec get(String.t()) :: {:ok, map()} | {:error, :not_found}
def get(code) when is_binary(code) do
case :ets.lookup(@table_name, code) do
[{^code, session}] -> {:ok, session}
[] -> {:error, :not_found}
end
end
@doc """
Updates an existing session.
## Returns
- `:ok` on success
- `{:error, :not_found}` if session doesn't exist
"""
@spec update(String.t(), map()) :: :ok | {:error, :not_found}
def update(code, session) when is_binary(code) and is_map(session) do
case :ets.lookup(@table_name, code) do
[{^code, _}] ->
:ets.insert(@table_name, {code, session})
:ok
[] ->
{:error, :not_found}
end
end
@doc """
Deletes a session by code.
"""
@spec delete(String.t()) :: :ok
def delete(code) when is_binary(code) do
:ets.delete(@table_name, code)
:ok
end
@doc """
Counts active sessions.
All sessions in the store are active (sessions are deleted when owner process terminates).
"""
@spec count_active() :: non_neg_integer()
def count_active do
:ets.info(@table_name, :size)
end
@doc """
Lists all active sessions.
Useful for debugging and admin interfaces.
All sessions in the store are active (sessions are deleted when owner process terminates).
"""
@spec list_active() :: [map()]
def list_active do
:ets.tab2list(@table_name)
|> Enum.map(fn {_code, session} -> session end)
|> Enum.sort_by(& &1.created_at, {:desc, DateTime})
end
# ===========================================
# GENSERVER CALLBACKS
# ===========================================
@impl true
def init(_opts) do
# Create ETS table with public access for fast reads
:ets.new(@table_name, [
:set,
:named_table,
:public,
read_concurrency: true,
write_concurrency: true
])
# Create monitors table to track ref -> code mappings
:ets.new(@monitors_table, [
:set,
:named_table,
:protected
])
# Schedule periodic cleanup of orphaned sessions (fallback safety)
schedule_cleanup()
Logger.debug("Sync.SessionStore started")
{:ok, %{}}
end
@impl true
def handle_cast({:monitor_session, code, pid}, state) do
ref = Process.monitor(pid)
# Store mapping from monitor ref to session code
:ets.insert(@monitors_table, {ref, code})
Logger.debug("Sync.SessionStore: Monitoring #{inspect(pid)} for session #{code}")
{:noreply, state}
end
@impl true
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
# Process died, delete the associated session
case :ets.lookup(@monitors_table, ref) do
[{^ref, code}] ->
:ets.delete(@monitors_table, ref)
:ets.delete(@table_name, code)
Logger.debug("Sync.SessionStore: Session #{code} deleted (owner process terminated)")
[] ->
:ok
end
{:noreply, state}
end
@impl true
def handle_info(:cleanup_orphaned, state) do
cleanup_orphaned_sessions()
schedule_cleanup()
{:noreply, state}
end
@impl true
def handle_info(_msg, state) do
{:noreply, state}
end
# ===========================================
# PRIVATE FUNCTIONS
# ===========================================
defp schedule_cleanup do
Process.send_after(self(), :cleanup_orphaned, @cleanup_interval)
end
defp cleanup_orphaned_sessions do
# This is a fallback cleanup for any orphaned sessions
# (e.g., if monitor somehow failed to fire)
# Sessions without owner_pid or with dead owner_pid are cleaned up
orphaned =
:ets.foldl(
fn {code, session}, acc ->
case Map.get(session, :owner_pid) do
nil ->
# Session without owner - check if it's very old (created > 24h ago)
if session_too_old?(session) do
[code | acc]
else
acc
end
pid when is_pid(pid) ->
# Check if the process is still alive
if Process.alive?(pid) do
acc
else
[code | acc]
end
end
end,
[],
@table_name
)
# Delete orphaned sessions
Enum.each(orphaned, &:ets.delete(@table_name, &1))
if orphaned != [] do
Logger.debug("Sync.SessionStore: Cleaned up #{length(orphaned)} orphaned sessions")
end
end
defp session_too_old?(session) do
case Map.get(session, :created_at) do
nil ->
true
created_at ->
# Consider sessions older than 24 hours as orphaned
hours_old = DateTime.diff(DateTime.utc_now(), created_at, :hour)
hours_old > 24
end
end
end