# ServerChatTracker Singleton GenServer that acts as a per-user process registry for `ServerChat` instances. ## Overview `ServerChatTracker` ensures each user has at most one `ServerChat` process running at a time. It maintains a bidirectional mapping between user IDs and process identifiers, and uses `Process.monitor/1` to automatically detect crashes and clean up stale entries. The tracker is registered under its own module name and started as part of the supervision tree. Chat processes are created lazily on the first `get_or_start/1` call for a given user. ## Features | Feature | Description | |---------|-------------| | Lazy startup | ServerChat processes are only started when first requested | | At-most-one guarantee | Each user_id maps to exactly one ServerChat pid | | Automatic cleanup | Monitor-based crash detection removes terminated processes | | Graceful shutdown | `stop_client/1` demonitors, stops the process, and cleans up state | ## GenServer State The state is a map with two entries that form a bidirectional mapping: | Key | Type | Description | |-----|------|-------------| | `:clients` | `%{user_id => pid}` | Forward lookup: user to their chat process | | `:refs` | `%{reference => user_id}` | Reverse lookup: monitor reference to user | ```elixir %{ clients: %{1 => #PID<0.450.0>, 7 => #PID<0.512.0>}, refs: %{#Reference<0.1234> => 1, #Reference<0.5678> => 7} } ``` ## Public API ### start_link/1 Start the tracker as a named singleton. ```elixir {:ok, pid} = ServerChatTracker.start_link() ``` ### get_or_start/1 Return the existing `ServerChat` pid for a user, or start a new one. ```elixir {:ok, pid} = ServerChatTracker.get_or_start(user_id) # Subsequent calls return the same pid {:ok, ^pid} = ServerChatTracker.get_or_start(user_id) ``` **Returns:** - `{:ok, pid}` — existing or newly started ServerChat process - `{:error, reason}` — if `ServerChat.start_link/1` fails - `{:error, {:exception, message}}` — if start_link raises ### stop_client/1 Gracefully stop a user's ServerChat and remove it from tracking. ```elixir :ok = ServerChatTracker.stop_client(user_id) {:error, :not_found} = ServerChatTracker.stop_client(999) ``` **Returns:** - `:ok` — process stopped and removed - `{:error, :not_found}` — no tracked process for that user ### list_clients/0 Return the full `clients` map of all tracked user_id => pid pairs. ```elixir clients = ServerChatTracker.list_clients() # => %{1 => #PID<0.450.0>, 7 => #PID<0.512.0>} ``` ## Internal Behaviour ### Process Monitoring When a new `ServerChat` is started via `get_or_start/1`, the tracker calls `Process.monitor/1` on the new pid. If the `ServerChat` process terminates for any reason (crash, timeout, explicit stop outside the tracker), the tracker receives a `:DOWN` message and automatically removes the user from both the `clients` and `refs` maps. ### Graceful Stop Flow When `stop_client/1` is called: 1. Looks up the pid in `clients` 2. Finds the monitor reference via reverse lookup in `refs` 3. Calls `Process.demonitor(ref, [:flush])` to cancel the monitor and discard any pending `:DOWN` 4. Calls `ServerChat.stop(pid)` for a clean GenServer shutdown 5. Removes entries from both `clients` and `refs` ## Dependencies | Module | Purpose | |--------|---------| | `ServerChat` | Per-user chat GenServer that this module tracks | | `Logger` | Logs process start, stop, and crash events |