defmodule Sorcery.Service do @moduledoc """ Defines a service that can handle events from domains. Services are used to handle side effects (like sending emails) or to maintain read models (views). ## Auto-Starting Services can be configured to auto-start when your application boots. This ensures that no events are missed and read models stay consistent. Configure auto-starting in your config: config :sorcery, :auto_start, services: [ MyApp.NotificationService, MyApp.ReadModelService ] Services not configured for auto-start must be started manually. ## Example defmodule MyApp.NotificationService do use Sorcery.Service service do on UserRegistered do send_welcome_email(event) end on OrderPlaced do send_order_confirmation(event) end end defp send_welcome_email(event) do # Send email logic end defp send_order_confirmation(event) do # Send confirmation logic end end """ defmacro __using__(_opts) do quote do import Sorcery.Service, only: [service: 1] Module.register_attribute(__MODULE__, :event_handlers, accumulate: true) @before_compile Sorcery.Service end end defmacro service(do: block) do quote do import Sorcery.Service unquote(block) end end defmacro on(event_module, do: handler) do quote do handler_fn = unquote(Macro.escape(handler)) Module.put_attribute(__MODULE__, :event_handlers, {unquote(event_module), handler_fn}) end end defmacro __before_compile__(env) do event_handlers = Module.get_attribute(env.module, :event_handlers) handle_event_clauses = for {event_module, handler} <- event_handlers do quote do def handle_event(%unquote(event_module){} = event) do unquote(handler).(event) end end end default_handle_event = quote do def handle_event(_), do: [] end quote do use GenServer def start_link(opts \\ []) do name = Keyword.get(opts, :name, __MODULE__) GenServer.start_link(__MODULE__, [], name: name) end def init(_) do # Subscribe to the event store Sorcery.EventStore.subscribe() {:ok, %{}} end def handle_info({:event, event}, state) do # Convert the event data back to a struct domain_event = Sorcery.EventConverter.event_store_to_domain(event) # Handle the event service_events = handle_event(domain_event) unless service_events == [] do service_events |> Enum.map(&Sorcery.EventConverter.domain_to_event_store(&1, event.metadata.domain, event.metadata.instance_id)) |> Sorcery.EventStore.append() end {:noreply, state} end unquote_splicing(handle_event_clauses) unquote(default_handle_event) end end end