jonnify·bcs /bcs/elixir-core/otp-application/the-property-store

B2.1.6 · dive 6 of 6 · module B2.1

The property store, in full.

EchoData.Bcs.PropertyStore: start_link, the export list, init opening one :private :ordered_set, every ingress gated, page_desc walking the keyspace, placement pure. The system, entire.

A write crosses the boundary, the gate, and into the private table; placement stays pure. Select a part.
a callercall {:put, id, v} PropertyStore (GenServer) Bcs.gate(id, ns)wrong ns: refused :ets :ordered_set:private placement(id): parse + hash — pure, no callUSR0KHTOWnGLuC → 234878118
readout · Select a part. The boundary is GenServer calls and one cast; every ingress crosses Bcs.gate; admitted writes land in a :private :ordered_set; placement is pure.
§1

The idea

echo_data · bcs/property_store.ex

EchoData.Bcs.PropertyStore is the whole system in one file. start_link/1 reads its name and namespace; init/1 proves the contract once more for this process, then opens a single :ets table as :ordered_set and :private; and the state is two fields, the namespace and the table. The export list — put, get, window, page_desc, record_entity, placement — is the boundary B2.1.1 described.

Every write and read crosses the gate first. Each handle_call and the one handle_cast ask Bcs.gate to admit the id’s namespace before touching the table, so an id of the wrong namespace is refused at the door, not stored and found wrong later. window/3 gates both bounds and runs an :ets.select over the half-open range; page_desc/2 walks descending from the table’s last key. One function stands apart: placement/1 is pure, parsing and hashing the id with no call to the process at all.

This is the file Codemojex’s rooms are built from. A room is a property store keyed by player and round ids, its writes gated by the room’s namespace, its recent history read straight off the ordered keyspace by page_desc — no timestamp column, because the id carries the time. Read it once and the rest of the core is variations on it: the CHAMP map, the edges store, and the gates are this same discipline, specialized.

§2

The source

echo_data · committed, verbatim

echo_data/lib/echo_data/bcs/property_store.ex — complete, 103 lines

defmodule EchoData.Bcs.PropertyStore do
  @moduledoc """
  A BCS system in skeleton: a GenServer owning one private ordered_set
  property table keyed by the 14-byte branded string. Rung bcs1.1.
  """

  use GenServer

  alias EchoData.{BrandedId, Bcs}

  def start_link(opts) do
    name = Keyword.fetch!(opts, :name)
    ns = Keyword.fetch!(opts, :namespace)
    GenServer.start_link(__MODULE__, ns, name: name)
  end

  def put(store, id, value) when is_binary(id), do: GenServer.call(store, {:put, id, value})

  def get(store, id) when is_binary(id), do: GenServer.call(store, {:get, id})

  def page_desc(store, n) when is_integer(n) and n > 0,
    do: GenServer.call(store, {:page_desc, n})

  def record_entity(store, id) when is_binary(id), do: GenServer.cast(store, {:entity, id})

  @doc """
  Ascending ids in [lo, hi) — Chapter 1.5's synthetic cursors landed on the
  ordered_set. Bounds are branded ids (synthetic via min_for or real) and are
  gated like any ingress. Added by the Chapter 2.2 architecture review.
  """
  def window(store, lo, hi) when is_binary(lo) and is_binary(hi),
    do: GenServer.call(store, {:window, lo, hi})

  @spec placement(binary()) :: {:ok, non_neg_integer()} | {:error, :invalid}
  def placement(id) when is_binary(id) do
    case BrandedId.parse(id) do
      {:ok, _ns, snow} -> {:ok, BrandedId.hash32(snow)}
      :error -> {:error, :invalid}
    end
  end

  @impl true
  def init(ns) do
    {:ok, _mode} = BrandedId.self_check!()
    table = :ets.new(:bcs_props, [:ordered_set, :private])
    {:ok, %{ns: ns, table: table}}
  end

  @impl true
  def handle_call({:put, id, value}, _from, s) do
    case Bcs.gate(id, s.ns) do
      {:ok, _snow} ->
        :ets.insert(s.table, {id, value})
        {:reply, :ok, s}

      {:error, _} = err ->
        {:reply, err, s}
    end
  end

  def handle_call({:get, id}, _from, s) do
    case Bcs.gate(id, s.ns) do
      {:ok, _snow} ->
        case :ets.lookup(s.table, id) do
          [{^id, value}] -> {:reply, {:ok, value}, s}
          [] -> {:reply, {:error, :not_found}, s}
        end

      {:error, _} = err ->
        {:reply, err, s}
    end
  end

  def handle_call({:window, lo, hi}, _from, s) do
    with {:ok, _} <- EchoData.Bcs.gate(lo, s.ns),
         {:ok, _} <- EchoData.Bcs.gate(hi, s.ns) do
      spec = [{{:"$1", :_}, [{:>=, :"$1", {:const, lo}}, {:<, :"$1", {:const, hi}}], [:"$1"]}]
      {:reply, {:ok, :ets.select(s.table, spec)}, s}
    else
      {:error, _} = err -> {:reply, err, s}
    end
  end

  def handle_call({:page_desc, n}, _from, s) do
    {:reply, {:ok, walk_desc(s.table, :ets.last(s.table), n)}, s}
  end

  @impl true
  def handle_cast({:entity, id}, s) do
    case Bcs.gate(id, s.ns) do
      {:ok, _snow} -> :ets.insert(s.table, {id, true})
      {:error, _} -> :ok
    end

    {:noreply, s}
  end

  defp walk_desc(_table, :"$end_of_table", _n), do: []
  defp walk_desc(_table, _key, 0), do: []

  defp walk_desc(table, key, n),
    do: [key | walk_desc(table, :ets.prev(table, key), n - 1)]
end
§4

The doors

the sibling courses
§3

References

sources · related

Sources

Related