defmodule Sorcery.Domain.Server do @moduledoc """ The server component that manages domain instances and processes commands. The DomainServer is responsible for: 1. Managing domain process lifecycles 2. Processing commands and generating events 3. Maintaining domain state by applying events 4. Handling event subscriptions ## Types of Domains Sorcery supports two types of domains: 1. **ID-Based Domains** - Multiple instances of the same domain type, each with a unique ID 2. **Singleton Domains** - Single instance domains that manage global state ## Auto-Starting Domains can be configured to auto-start when your application boots. This ensures that: 1. All domains are ready to handle commands immediately 2. Domains listening for events from other domains don't miss any events 3. Event handlers are always active Configure auto-starting in your config: config :sorcery, :auto_start, domains: [ MyApp.UserDomain, MyApp.SettingsDomain ] Domains not configured for auto-start will be started on-demand when they receive their first command. ## ID-Based Domains Use these when you need multiple instances of the same domain type: ```elixir # Start and execute a command on a user domain Sorcery.DomainServer.execute( MyApp.UserDomain, "user-123", %MyApp.UserDomain.Commands.RegisterUser{ name: "John Doe", email: "john@example.com" } ) ``` ## Singleton Domains Use these for global state or configuration: ```elixir # Start and execute a command on a settings domain Sorcery.DomainServer.execute( MyApp.SettingsDomain, %MyApp.SettingsDomain.Commands.UpdateTheme{ theme: "dark" } ) ``` ## Process Management Each domain instance is a separate process that: - Subscribes to the event store - Maintains its own state - Processes commands - Generates and applies events ## Event Handling When events occur: 1. The event is stored in the event store 2. All domain processes receive the event 3. Each domain filters events relevant to it 4. Relevant events are applied to update state ## Process Registry Domain processes are registered with unique names: - ID-based domains: `{DomainModule, "id"}` - Singleton domains: `DomainModule` This ensures: - Only one process per domain instance - Easy lookup of existing processes - Automatic process cleanup on termination """ use GenServer @doc """ Starts a singleton instance of a domain. This is used for auto-starting domains at application boot. """ def start_singleton(domain_module) do name = via_tuple(domain_module, nil) case GenServer.start_link(__MODULE__, {domain_module, "singleton"}, name: name) do {:ok, pid} -> {:ok, pid} {:error, {:already_started, pid}} -> {:ok, pid} error -> error end end @doc """ Starts a new DomainServer process. ## Arguments Can be called in two ways: 1. With module and ID: `start_link(domain_module, id_or_command, maybe_command)` 2. With keyword options: `start_link(domain: domain_module, id: id)` """ def start_link(opts) when is_list(opts) do domain_module = Keyword.fetch!(opts, :domain) id = Keyword.get(opts, :id) start_link(domain_module, id) end def start_link(domain_module, id_or_command, maybe_command \\ nil) do case {id_or_command, maybe_command} do {id, _command} when is_binary(id) and not is_nil(maybe_command) -> # Case 1: ID-based domain name = via_tuple(domain_module, id) GenServer.start_link(__MODULE__, {domain_module, id}, name: name) {_command, nil} -> # Case 2: Singleton domain start_singleton(domain_module) end end @doc """ Gets the current state of a domain instance. ## Arguments * `domain_module` - The domain module * `id` - The instance ID (or nil for singleton domains) ## Returns * `{:ok, state}` - The current state of the domain * `{:error, :not_found}` - If the domain instance doesn't exist """ def get_state(domain_module, id) do name = via_tuple(domain_module, id) try do state = GenServer.call(name, :get_state) {:ok, state} catch :exit, {:noproc, _} -> {:error, :not_found} end end @doc """ Initializes a domain process with its initial state. The process will: 1. Subscribe to the event store 2. Load historical events 3. Build initial state by applying events """ def init({domain_module, id}) do Sorcery.EventStore.subscribe() # Load history and build initial state {:ok, events} = Sorcery.EventStore.get_events() state = events |> Enum.filter(fn event -> if id do event.metadata["instance_id"] == id else event.metadata["domain"] == domain_module end end) |> Enum.reduce(domain_module.new(), fn event, state -> # Event Store -> Domain Event try do # Convert the event data to the proper struct event_module = Module.concat([domain_module, Events, Macro.camelize(event.type)]) # Convert string keys to atoms in event data event_data = for {key, val} <- event.data, into: %{} do {String.to_existing_atom(key), val} end event_struct = struct(event_module, event_data) # Apply the event using the domain module domain_module.apply_event(state, event_struct) rescue e -> IO.inspect(e, label: "Error applying event") state end end) {:ok, {domain_module, id, state}} end def handle_call(:get_state, _from, {_domain_module, _id, state} = full_state) do {:reply, state, full_state} end def handle_call({:command, command}, _from, {domain_module, id, state}) do case domain_module.handle_command(state, command) do {:ok, events} -> # Domain Event -> Event Store # Only append events, don't apply them directly events |> Enum.map(&Sorcery.EventConverter.domain_to_event_store(&1, domain_module, id)) |> Sorcery.EventStore.append() {:reply, :ok, {domain_module, id, state}} {:error, reason} -> {:reply, {:error, reason}, {domain_module, id, state}} end end def handle_info({:event, event}, {domain_module, id, state} = current_state) do # For ID-based domains, only apply events that belong to this instance # For singleton domains, apply all events for this domain should_apply = if id do event.metadata.instance_id == id else event.metadata.domain == domain_module end if should_apply do # Event Store -> Domain Event event_struct = Sorcery.EventConverter.event_store_to_domain(event) new_state = domain_module.apply_event(state, event_struct) {:noreply, {domain_module, id, new_state}} else {:noreply, current_state} end end defp via_tuple(domain_module, id) do registry_key = if id, do: {domain_module, id}, else: domain_module {:via, Registry, {Sorcery.Domain.Registry, registry_key}} end @doc """ Executes a command on a domain. This is the main entry point for changing domain state. It will: 1. Start the domain process if it doesn't exist 2. Send the command to the process 3. Handle command validation 4. Generate and store events 5. Apply events to update state ## Examples For ID-based domains: Sorcery.DomainServer.execute( MyApp.UserDomain, "user-123", %MyApp.UserDomain.Commands.RegisterUser{ name: "John Doe", email: "john@example.com" } ) For singleton domains: Sorcery.DomainServer.execute( MyApp.SettingsDomain, %MyApp.SettingsDomain.Commands.UpdateTheme{ theme: "dark" } ) ## Returns * `:ok` - Command succeeded * `{:error, reason}` - Command failed with reason """ def execute(domain_module, id_or_command, maybe_command \\ nil) do case {id_or_command, maybe_command} do {id, command} when is_binary(id) and not is_nil(command) -> # Case 1: ID-based domain case start_link(domain_module, id, command) do {:ok, pid} -> GenServer.call(pid, {:command, command}) {:error, {:already_started, pid}} -> GenServer.call(pid, {:command, command}) error -> error end {command, nil} -> # Case 2: Singleton domain case start_link(domain_module, command) do {:ok, pid} -> GenServer.call(pid, {:command, command}) {:error, {:already_started, pid}} -> GenServer.call(pid, {:command, command}) error -> error end end end end