KurrentDB Erlang

An Erlang runtime backend for the sans-IO kurrentdb package.

Package Version Hex Docs

gleam add kurrentdb_erlang@1

kurrentdb_erlang uses gleam_hackney for HTTP/2 transport and gleam_otp for operation workers. A Connection is an OTP factory supervisor. Each KurrentDB operation starts a temporary worker that builds the request, sends it, decodes the response, and replies to the caller.

Starting a Connection

For scripts, tests, or small applications, start the backend supervisor directly.

import gleam/hackney
import gleam/option
import kurrentdb
import kurrentdb_erlang

pub fn start() {
  let assert Ok(client) =
    kurrentdb.from_connection_string(
      "kurrentdb://admin:changeit@localhost:2113?tls=false",
    )

  kurrentdb_erlang.start(client, hackney.configure(), option.None)
}

For applications, add the backend to your OTP supervision tree before any children that need to use KurrentDB.

import gleam/erlang/process
import gleam/hackney
import gleam/otp/factory_supervisor as factory
import gleam/otp/static_supervisor as supervisor
import kurrentdb
import kurrentdb_erlang

pub fn start_application_supervisor(
  name: process.Name(factory.Message(
    kurrentdb_erlang.WorkerStart,
    process.Subject(kurrentdb_erlang.WorkerMessage),
  )),
) {
  let client = kurrentdb.new("localhost", 2113, kurrentdb.TlsDisabled)

  let kurrentdb_child =
    kurrentdb_erlang.supervised(
      hackney.configure(),
      client,
      name,
    )

  supervisor.new(supervisor.RestForOne)
  |> supervisor.add(kurrentdb_child)
  // |> supervisor.add(other)
  // |> supervisor.add(application)
  // |> supervisor.add(children)
  |> supervisor.start
}

pub fn connection(
  name: process.Name(factory.Message(
    kurrentdb_erlang.WorkerStart,
    process.Subject(kurrentdb_erlang.WorkerMessage),
  )),
) -> kurrentdb_erlang.Connection {
  kurrentdb_erlang.from_name(name)
}

The name type is the same Name used by supervised and from_name:

process.Name(
  factory.Message(
    kurrentdb_erlang.WorkerStart,
    process.Subject(kurrentdb_erlang.WorkerMessage),
  ),
)

You normally create that name once at your application boundary and pass it to both supervised and from_name.

Appending Events

Unary operations return a Task. Use await to receive the result.

import gleam/json
import kurrentdb/operation/append_to_stream
import kurrentdb_erlang
import youid/uuid

pub fn append_order(connection: kurrentdb_erlang.Connection) {
  let event =
    append_to_stream.json_event(
      uuid: uuid.v7(),
      event_type: "OrderPlaced",
      data: json.object([
        #("order_id", json.string("order-123")),
        #("total", json.float(49.95)),
      ]),
    )

  let task =
    kurrentdb_erlang.append_to_stream(
      connection,
      stream: "orders-123",
      events: [event],
      config: append_to_stream.configure(),
    )

  kurrentdb_erlang.await(task, within: 5000)
}

Reading Streams

Streaming operations return a Stream. Use receive to pull messages and close when the stream is no longer needed.

import kurrentdb
import kurrentdb/operation/read_stream
import kurrentdb_erlang

pub fn read_one(
  connection: kurrentdb_erlang.Connection,
  client: kurrentdb.Client,
) {
  let stream =
    kurrentdb_erlang.read_stream(
      connection,
      client,
      stream: "orders-123",
      config: read_stream.configure() |> read_stream.max_count(1),
    )

  let result = kurrentdb_erlang.receive(stream, within: 5000)
  kurrentdb_erlang.close(stream)
  result
}

receive returns StreamMessage values. ReadMessage contains the full KurrentDB read message. ReadEvent is also emitted as a convenience whenever a read message contains an event.

Subscriptions

You can subscribe by polling messages yourself.

import kurrentdb
import kurrentdb/operation/subscribe_to_stream
import kurrentdb_erlang

pub fn subscribe(
  connection: kurrentdb_erlang.Connection,
  client: kurrentdb.Client,
) {
  kurrentdb_erlang.subscribe_to_stream(
    connection,
    client,
    stream: "orders-123",
    config: subscribe_to_stream.configure(),
  )
}

Or you can use a callback-style event subscription when you only need events. Non-event messages such as confirmations and checkpoints are ignored by this API.

import kurrentdb
import kurrentdb/operation/subscribe_to_all
import kurrentdb_erlang

pub fn subscribe_to_all_events(
  connection: kurrentdb_erlang.Connection,
  client: kurrentdb.Client,
) {
  kurrentdb_erlang.subscribe_to_all_events(
    connection,
    client,
    config: subscribe_to_all.configure(),
    on_event: fn(event) {
      // Handle event here.
      Nil
    },
  )
}

Close callback subscriptions with close_subscription.

Stream Metadata

Metadata is written by appending a $metadata event to the stream’s metadata stream.

import gleam/json
import kurrentdb/operation/append_to_stream
import kurrentdb/stream_metadata
import kurrentdb_erlang
import youid/uuid

pub fn set_metadata(connection: kurrentdb_erlang.Connection) {
  let metadata =
    stream_metadata.new()
    |> stream_metadata.max_count(100)
    |> stream_metadata.custom("owner", json.string("billing"))

  let task =
    kurrentdb_erlang.set_stream_metadata(
      connection,
      stream: "orders-123",
      metadata: metadata,
      uuid: uuid.v7(),
      config: append_to_stream.configure(),
    )

  kurrentdb_erlang.await(task, within: 5000)
}

TLS

The kurrentdb.Client controls whether generated requests use HTTP or HTTPS. The hackney.Configuration controls transport details such as certificate verification.

For local TLS development with the included Docker setup, generate certificates and configure Hackney to trust the generated CA certificate.

let http_config =
  hackney.configure()
  |> hackney.verify_ca_certificate_file("certs/ca.crt")

Development

./scripts/generate-dev-certs.sh
docker compose -f docker-compose.yml up -d
gleam test

Further documentation can be found at https://hexdocs.pm/kurrentdb_erlang.

Search Document