Skip to content

Cratis/Chronicle.Elixir

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Chronicle Elixir Client

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.ReadModel — define read models with model-bound 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

This guide uses projections as the default because they run inside Chronicle and keep read model updates close to the event store.

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

  alias MyApp.Events.{AccountOpened, FundsDeposited}

  defstruct account_id: nil, owner_name: nil, balance: 0

  from AccountOpened,
    set: [
      account_id: :event_source_id,
      owner_name: :owner_name,
      balance: :initial_balance
    ]

  from FundsDeposited,
    add: [balance: :amount]
end

3. Define projection mappings (recommended)

Projection mappings are registered on Chronicle and executed server-side. Each from/2 maps an event type to:

  • A read model key ($eventSourceId by default when :key is omitted)
  • A set of property assignments
  • Optional expressions that can use event fields and existing model values

That means Chronicle can maintain read models directly from the event stream without reducer code running in your client process.

The projection mapping is declared directly in the read model module using from, join, removed_with, and from_every.

For expressions, atoms are preferred and more natural:

  • :owner_name, :amount for event fields
  • :event_source_id, :occurred for built-in context values
  • string expressions only for advanced cases

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

If your Chronicle artifacts are defined in one OTP app, use otp_app and let Chronicle discover event types, reactors, reducers, and read models automatically.

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",
        otp_app: :my_app}
    ]

    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)

Quick Start (Reducer Alternative)

Use reducers when you want the read model folding logic in Elixir code in your app process. In this mode, Chronicle streams events to the reducer and your reducer returns the next model state.

1. 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

2. Start Chronicle.Client with reducers

With auto-discovery:

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",
        otp_app: :my_app}
    ]

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

Connection Strings

Chronicle connection strings use the chronicle:// scheme:

Format Use
chronicle://localhost:35000 No authentication (development)
chronicle://localhost:35000?disableTls=true Disable TLS for local dev
chronicle://client-id:secret@server:35000 Client credentials
chronicle://server:35000?apiKey=my-key API key authentication
chronicle+srv://service-name:35000 SRV 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

Projections are the recommended default. They are model-bound mappings declared on Chronicle.ReadModel and executed server-side by Chronicle:

defmodule MyApp.ReadModels.Account do
  use Chronicle.ReadModel

  alias MyApp.Events.{AccountOpened, FundsDeposited}

  defstruct account_id: nil, owner_name: nil, balance: 0

  from AccountOpened,
    set: [
      account_id: :event_source_id,
      owner_name: :owner_name,
      balance: :initial_balance
    ]

  from FundsDeposited,
    add: [balance: :amount]
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
        artifacts.ex      # Artifact auto-discovery helpers
        event_type.ex     # use Chronicle.EventType macro
        reactor.ex        # use Chronicle.Reactor behaviour
        reducer.ex        # use Chronicle.Reducer behaviour
        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.

About

Chronicle idiomatic client for Elixir

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors