Skip to main content

lib/dcb/store.ex

defmodule Dcb.Store do
  @moduledoc """
  Public API for the DCB event store backed by FoundationDB.

  This module wraps the raw Rust NIF bindings with idiomatic Elixir
  defaults and option handling.

  See the [README](readme.html) for data structure shapes (event, query, condition).
  """

  alias Dcb.Native

  @doc """
  Opens a store connection scoped to `namespace`.

  ## Options

    * `:cluster_file` - path to the FDB cluster file (default: `nil`, uses the default cluster file)

  ## Example

      {:ok, store} = Dcb.Store.open("my_namespace")
      {:ok, store} = Dcb.Store.open("my_namespace", cluster_file: "/etc/foundationdb/fdb.cluster")
  """
  def open(namespace, opts \\ []) do
    cluster_file = Keyword.get(opts, :cluster_file, nil)
    Native.dcb_store_open(cluster_file, namespace)
  end

  @doc """
  Appends `events` to the store.

  Optionally accepts `conditions` for optimistic concurrency: the append
  fails if any matching events exist after the position specified in each condition.

  ## Example

      events = [%{type_name: "UserCreated", tags: ["user-1"], data: <<>>}]
      {:ok, position} = Dcb.Store.append(store, events)

      condition = %{query: %{items: [%{types: ["UserCreated"], tags: ["user-1"]}]}, after: nil}
      {:ok, position} = Dcb.Store.append(store, events, [condition])
  """
  def append(store, events, conditions \\ []) do
    Native.dcb_store_append(store, events, conditions)
  end

  @doc """
  Reads events matching `query` from the store.

  ## Options

    * `:limit` - max events to return, `0` means unlimited (default: `0`)
    * `:after` - only return events after this position (default: `nil`)
    * `:reverse` - return events in reverse order (default: `false`)

  ## Example

      query = %{items: [%{types: ["UserCreated"], tags: ["user-1"]}]}
      {:ok, events} = Dcb.Store.read(store, query)
      {:ok, events} = Dcb.Store.read(store, query, limit: 50, after: position)
  """
  def read(store, query, opts \\ []) do
    read_opts = %{
      limit: Keyword.get(opts, :limit, 0),
      after: Keyword.get(opts, :after, nil),
      reverse: Keyword.get(opts, :reverse, false)
    }

    Native.dcb_store_read(store, query, read_opts)
  end

  @doc "Returns all events in the store, in insertion order."
  def read_all(store), do: Native.dcb_store_read_all(store)

  @doc """
  Subscribes the calling process to store change notifications.

  Sends `{:dcb_store_changed, store}` to `self()` whenever new events are appended.
  The watch is one-shot; call `watch/1` again after receiving the message to re-subscribe.
  """
  def watch(store), do: Native.dcb_store_watch(store, self())

  @doc "Returns the position stored under `name` for the given named cursor."
  def get_cursor(store, name), do: Native.dcb_store_get_cursor(store, name)

  @doc "Persists `position` under `name` as a named cursor."
  def set_cursor(store, name, position), do: Native.dcb_store_set_cursor(store, name, position)
end