lib/chronicle/reactor.ex

# Copyright (c) Cratis. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.

defmodule Chronicle.Reactor do
  @moduledoc """
  Behaviour and macro for defining Chronicle reactors.

  Reactors observe events and react to them with side effects — sending emails,
  calling external APIs, updating caches, etc. They run in Chronicle's
  observation pipeline and receive events as they are appended.

  ## Defining a reactor

  Use `Chronicle.Reactor` in a module and implement the `handle/2` callback.
  Declare which event types the reactor handles using the `@handles` module
  attribute.

      defmodule MyApp.Reactors.NotificationReactor do
        use Chronicle.Reactor

        @handles MyApp.Events.AccountOpened
        @handles MyApp.Events.FundsDeposited

        @impl true
        def handle(%MyApp.Events.AccountOpened{} = event, _context) do
          # Send welcome email
          MyApp.Mailer.send_welcome(event.owner_name)
          :ok
        end

        def handle(%MyApp.Events.FundsDeposited{} = event, _context) do
          # Notify account holder
          :ok
        end
      end

  ## Options for `use Chronicle.Reactor`

    * `:id` — a stable string identifier for this reactor. Defaults to the
      module's full name. Changing this value causes Chronicle to treat this as
      a different reactor and will reset its observation position.

  ## Registering with Chronicle.Client

      {Chronicle.Client,
        ...
        reactors: [MyApp.Reactors.NotificationReactor]}

  ## Event context

  The second argument to `handle/2` is a map with the following keys:

    * `:event_source_id` — the event source (e.g. aggregate ID)
    * `:sequence_number` — the event's position in the event log
    * `:occurred` — when the event was appended (ISO 8601 string)
    * `:event_store` — the event store name
    * `:namespace` — the namespace
    * `:correlation_id` — the correlation ID for the append operation

  ## Return values

  `handle/2` must return `:ok` on success, or `{:error, reason}` on failure.
  Failures are reported back to Chronicle as a failed partition, which can be
  retried or replayed.
  """

  @doc """
  Handles an event dispatched by Chronicle.

  Called once per event for each partition. Must return `:ok` or `{:error, reason}`.
  """
  @callback handle(event :: struct(), context :: map()) :: :ok | {:error, term()}

  defmacro __using__(opts) do
    quote bind_quoted: [opts: opts] do
      @behaviour Chronicle.Reactor

      Module.register_attribute(__MODULE__, :handles, accumulate: true)

      @chronicle_reactor_id Keyword.get(opts, :id, __MODULE__ |> to_string())

      @before_compile Chronicle.Reactor
    end
  end

  defmacro __before_compile__(_env) do
    quote do
      @doc false
      def __chronicle_reactor__(:id), do: @chronicle_reactor_id
      def __chronicle_reactor__(:handles), do: @handles |> Enum.reverse()
    end
  end
end