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 | %{...}
}Single use
A code is claimed once (claim/2). That it was claimed is kept apart from
the session map, in a table of its own that only this process writes, so
no later status (:completed, :failed, or :pending written back by
update/2 or merge/2) makes the code claimable again. The claim lives
as long as the session does.
Summary
Functions
Returns a specification to start this module under a supervisor.
Marks a session as connected, once.
Counts active sessions.
Creates a new session in the store and monitors the owner process.
Deletes a session by code.
Gets a session by its code.
Lists all active sessions. Useful for debugging and admin interfaces.
Merges updates into an existing session, in the store's own process so
the merge cannot write back a copy read before a concurrent claim/2.
Starts the SessionStore GenServer.
Updates an existing session.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec claim(String.t(), DateTime.t()) :: {:ok, map()} | {:error, :already_used | :not_found}
Marks a session as connected, once.
The check and the write happen in the store's own process, so two callers racing on one code cannot both be told it is free. A code is claimed once for the life of its session, whatever its status becomes afterwards.
Returns
{:ok, session}- The session, now:connectedatconnected_at{:error, :already_used}- It was claimed before{:error, :not_found}- No such session
@spec count_active() :: non_neg_integer()
Counts active sessions.
All sessions in the store are active (sessions are deleted when owner process terminates).
@spec create(map()) :: :ok | {:error, :already_exists}
Creates a new session in the store and monitors the owner process.
Parameters
session- Map with at leastcodeandowner_pidfields
Returns
:okon success{:error, :already_exists}if code already exists
@spec delete(String.t()) :: :ok
Deletes a session by code.
Gets a session by its code.
Returns
{:ok, session}if found{:error, :not_found}if not found
@spec list_active() :: [map()]
Lists all active sessions. Useful for debugging and admin interfaces.
All sessions in the store are active (sessions are deleted when owner process terminates).
Merges updates into an existing session, in the store's own process so
the merge cannot write back a copy read before a concurrent claim/2.
Returns
{:ok, session}- The merged session{:error, :not_found}- No such session
@spec start_link(keyword()) :: GenServer.on_start()
Starts the SessionStore GenServer.
Updates an existing session.
Returns
:okon success{:error, :not_found}if session doesn't exist