Packages
Elixir client for SpacetimeDB — BSATN binary protocol, WebSocket subscriptions, reducer calls, live ETS table mirrors
Current section
Files
Jump to
Current section
Files
lib/spacetimedb.ex
defmodule SpacetimeDB do
@moduledoc ~S"""
Elixir client for [SpacetimeDB](https://spacetimedb.com).
## Quick start
{:ok, conn} = SpacetimeDB.start_link(
host: "localhost",
database: "my_module",
handler: %{
on_identity_token: fn token, _ ->
IO.puts("Connected as #{token.identity}")
end,
on_transaction_update: fn update, _ ->
IO.inspect(update.tables, label: "tables changed")
end
}
)
SpacetimeDB.subscribe(conn, ["SELECT * FROM Player"])
SpacetimeDB.call_reducer(conn, "CreatePlayer", ["Alice"])
## Using a handler module
defmodule MyHandler do
@behaviour SpacetimeDB.Handler
@impl true
def on_transaction_update(update, _arg) do
Enum.each(update.tables, &process_table/1)
end
end
{:ok, conn} = SpacetimeDB.start_link(
host: "prod.example.com",
port: 443,
tls: true,
database: "game-prod",
token: System.get_env("SPACETIMEDB_TOKEN"),
handler: MyHandler
)
## BSATN (binary protocol, default)
By default the connection uses `v1.bsatn.spacetimedb` — binary WebSocket frames
that are 3–5× smaller than JSON. Row data in `TableUpdate` structs arrives as
raw BSATN binaries; decode them with `SpacetimeDB.BSATN.Schema`:
defmodule MyGame.Player do
use SpacetimeDB.BSATN.Schema
bsatn_schema do
field :id, :u32
field :name, :string
field :health, :u32
end
end
def on_transaction_update(update, _) do
for table <- update.tables, table.table_name == "Player" do
for row_bin <- table.inserts do
{:ok, player, ""} = MyGame.Player.decode(row_bin)
IO.inspect(player)
end
end
end
Pass `protocol: :json` to use text frames and plain decoded JSON maps instead.
## Options
See `SpacetimeDB.Connection` for the full option reference.
"""
alias SpacetimeDB.Connection
@doc """
Start a connection process and link it to the caller.
The process is not supervised — wrap in your application's supervision tree
for production use:
children = [
{SpacetimeDB, host: "localhost", database: "my_module", handler: MyHandler}
]
"""
@spec start_link(keyword()) :: GenServer.on_start()
defdelegate start_link(opts), to: Connection
@doc "Subscribe to one or more SQL queries (legacy multi-query)."
@spec subscribe(GenServer.server(), [String.t()]) :: :ok
defdelegate subscribe(conn, query_strings), to: Connection
@doc "Subscribe to a single SQL query, identified by `query_id`."
@spec subscribe_single(GenServer.server(), String.t(), non_neg_integer()) :: :ok
defdelegate subscribe_single(conn, query, query_id), to: Connection
@doc "Subscribe to multiple SQL queries under one `query_id`."
@spec subscribe_multi(GenServer.server(), [String.t()], non_neg_integer()) :: :ok
defdelegate subscribe_multi(conn, query_strings, query_id), to: Connection
@doc "Unsubscribe from a query."
@spec unsubscribe(GenServer.server(), non_neg_integer()) :: :ok
defdelegate unsubscribe(conn, query_id), to: Connection
@doc """
Call a reducer.
Returns `{:ok, request_id}`. The result arrives asynchronously via the
handler's `on_transaction_update/2` callback; match on `reducer_call.request_id`.
"""
@spec call_reducer(GenServer.server(), String.t(), list(), non_neg_integer()) ::
{:ok, non_neg_integer()}
defdelegate call_reducer(conn, reducer, args, flags \\ 0), to: Connection
@doc """
Run a one-off SQL query without subscribing.
Returns `{:ok, message_id}` (16-byte binary). The result arrives
asynchronously via the handler's `on_one_off_query_response/2` callback.
"""
@spec one_off_query(GenServer.server(), String.t()) :: {:ok, binary()}
defdelegate one_off_query(conn, query_string), to: Connection
@doc "Return the current connection status: `:connected`, `:connecting`, or `:disconnected`."
@spec status(GenServer.server()) :: :connected | :connecting | :disconnected
defdelegate status(conn), to: Connection
@doc "Disconnect and stop the connection process."
@spec stop(GenServer.server()) :: :ok
defdelegate stop(conn), to: Connection
# Allow SpacetimeDB to be used directly as a child_spec in supervision trees.
@doc false
def child_spec(opts) do
%{
id: Keyword.get(opts, :name, __MODULE__),
start: {__MODULE__, :start_link, [opts]},
restart: :permanent,
type: :worker
}
end
end