Chronicle Elixir Client

Copy Markdown View Source

Idiomatic Elixir client for the Chronicle event-sourcing platform.

Overview

Chronicle is an event-sourcing kernel that stores domain events and projects them into read models. This library provides a clean, idiomatic Elixir interface built on top of the Chronicle gRPC API.

Key features:

  • use Chronicle.EventType — annotate structs as event types with stable IDs
  • use Chronicle.Reactor — react to events with side effects
  • use Chronicle.Reducer — build read models by folding events into state
  • use Chronicle.Projection — declare server-side read model projections
  • Resilient connection — automatic reconnection with exponential backoff
  • OTP-native — fits naturally in your supervision tree

Installation

Add the dependency to your mix.exs:

def deps do
  [
    {:cratis_chronicle, "~> 0.1"}
  ]
end

Quick Start

1. Define event types

defmodule MyApp.Events.AccountOpened do
  use Chronicle.EventType, id: "account-opened-v1"
  defstruct [:account_id, :owner_name, :initial_balance]
end

defmodule MyApp.Events.FundsDeposited do
  use Chronicle.EventType, id: "funds-deposited-v1"
  defstruct [:account_id, :amount]
end

2. Define a read model

defmodule MyApp.ReadModels.Account do
  use Chronicle.ReadModel
  defstruct account_id: nil, owner_name: nil, balance: 0
end

3. Define a reducer

Reducers fold events into a read model, one event at a time:

defmodule MyApp.Reducers.AccountReducer do
  use Chronicle.Reducer, model: MyApp.ReadModels.Account

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

  @impl true
  def reduce(%MyApp.Events.AccountOpened{} = event, _model, _context) do
    %MyApp.ReadModels.Account{
      account_id: event.account_id,
      owner_name: event.owner_name,
      balance: event.initial_balance
    }
  end

  def reduce(%MyApp.Events.FundsDeposited{} = event, model, _context) do
    %{model | balance: model.balance + event.amount}
  end
end

4. Define a reactor (optional)

Reactors react to events with side effects:

defmodule MyApp.Reactors.NotificationReactor do
  use Chronicle.Reactor

  @handles MyApp.Events.AccountOpened

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

5. Start Chronicle.Client in your supervision tree

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      {Chronicle.Client,
        connection_string: "chronicle://localhost:35000?disableTls=true",
        event_store: "my-app",
        event_types: [
          MyApp.Events.AccountOpened,
          MyApp.Events.FundsDeposited
        ],
        reactors: [MyApp.Reactors.NotificationReactor],
        reducers: [MyApp.Reducers.AccountReducer]}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

6. Append events and query read models

# Append a single event
:ok = Chronicle.append("account-42", %MyApp.Events.AccountOpened{
  account_id: "account-42",
  owner_name: "Alice",
  initial_balance: 1000
})

# Append multiple events atomically
:ok = Chronicle.append_many("account-42", [
  %MyApp.Events.FundsDeposited{account_id: "account-42", amount: 500},
  %MyApp.Events.FundsDeposited{account_id: "account-42", amount: 200}
])

# Read back the current read model
{:ok, account} = Chronicle.read_model(MyApp.ReadModels.Account, "account-42")
IO.inspect(account)
# => %MyApp.ReadModels.Account{account_id: "account-42", owner_name: "Alice", balance: 1700}

# Get all instances
{:ok, accounts} = Chronicle.all(MyApp.ReadModels.Account)

Connection Strings

Chronicle connection strings use the chronicle:// scheme:

FormatUse
chronicle://localhost:35000No authentication (development)
chronicle://localhost:35000?disableTls=trueDisable TLS for local dev
chronicle://client-id:secret@server:35000Client credentials
chronicle://server:35000?apiKey=my-keyAPI key authentication
chronicle+srv://service-name:35000SRV record lookup
alias Chronicle.Connections.ConnectionString

# Parse a string
cs = ConnectionString.parse("chronicle://localhost:35000?disableTls=true")

# Use helpers
cs = ConnectionString.default()      # chronicle://localhost:35000
cs = ConnectionString.development()  # includes dev credentials

# Modify
cs = ConnectionString.with_api_key(cs, "my-api-key")
cs = ConnectionString.with_credentials(cs, "client-id", "secret")

Declarative Projections

As an alternative to reducers, projections declare server-side property mappings. Chronicle executes them on the kernel, enabling richer query capabilities:

defmodule MyApp.Projections.AccountProjection do
  use Chronicle.Projection, model: MyApp.ReadModels.Account

  @impl true
  def define do
    import Chronicle.Projection.Builder

    new()
    |> from(MyApp.Events.AccountOpened,
        key: "$eventSourceId",
        properties: %{
          "account_id" => "$eventSourceId",
          "owner_name" => "OwnerName",
          "balance" => "InitialBalance"
        })
    |> from(MyApp.Events.FundsDeposited,
        key: "$eventSourceId",
        properties: %{"balance" => "$add(Amount, balance)"})
  end
end

Multiple clients

Run multiple Chronicle.Client instances for different event stores:

{Chronicle.Client,
  name: :bank,
  connection_string: "chronicle://bank-server:35000",
  event_store: "bank",
  event_types: [...]}

{Chronicle.Client,
  name: :crm,
  connection_string: "chronicle://crm-server:35000",
  event_store: "crm",
  event_types: [...]}

# Specify which client to use
Chronicle.append("customer-1", event, client: :crm)
Chronicle.read_model(Account, "account-1", client: :bank)

Running the Console Sample

A working example is in the Samples/console directory.

Prerequisites: A Chronicle kernel running locally on port 35000.

cd Samples/console
mix deps.get
mix run --no-halt

Set CHRONICLE_CONNECTION_STRING to override the default connection:

CHRONICLE_CONNECTION_STRING="chronicle://myserver:35000?apiKey=secret" mix run --no-halt

Local Development

Prerequisites

  • Elixir 1.14+ and OTP 25+
  • A running Chronicle kernel (see Chronicle)

Setup

cd Source/chronicle
mix deps.get
mix compile
mix test

Running tests

The unit tests do not require a running Chronicle instance:

mix test

Code formatting

mix format

Generating documentation

mix docs
open doc/index.html

Package structure

Source/
  chronicle/              # The cratis/chronicle Hex package
    lib/
      chronicle.ex        # Convenience API
      chronicle/
        connections/
          connection_string.ex
          connection.ex
        client.ex         # Supervisor entry point
        event_type.ex     # use Chronicle.EventType macro
        reactor.ex        # use Chronicle.Reactor behaviour
        reducer.ex        # use Chronicle.Reducer behaviour
        projection.ex     # use Chronicle.Projection behaviour
        projection/
          builder.ex      # Fluent projection builder
        read_model.ex     # use Chronicle.ReadModel macro
        event_log.ex      # Append and query events
        event_types.ex    # Register event types with Chronicle
        constraints.ex    # Register event constraints
        read_models.ex    # Query read model instances
        reactors/
          handler.ex      # gRPC streaming reactor handler
        reducers/
          handler.ex      # gRPC streaming reducer handler
        projections/
          registrar.ex    # Projection registration GenServer

Samples/
  console/                # Runnable console example

License

MIT — see LICENSE.