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 IDsuse Chronicle.Reactor— react to events with side effectsuse Chronicle.Reducer— build read models by folding events into stateuse 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"}
]
endQuick 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]
end2. Define a read model
defmodule MyApp.ReadModels.Account do
use Chronicle.ReadModel
defstruct account_id: nil, owner_name: nil, balance: 0
end3. 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
end4. 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
end5. 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
end6. 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:
| 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
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
endMultiple 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-haltLocal Development
Prerequisites
- Elixir 1.14+ and OTP 25+
- A running Chronicle kernel (see Chronicle)
Setup
cd Source/chronicle
mix deps.get
mix compile
mix testRunning tests
The unit tests do not require a running Chronicle instance:
mix testCode formatting
mix formatGenerating documentation
mix docs
open doc/index.htmlPackage 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 exampleLicense
MIT — see LICENSE.