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