The OapiCodemode.SpecStore the library ships with: one ETS table.
For tests, single-node deployments and small specs. The handle is the
table itself — new/0 creates it and hands it back — so the caller owns
it and its lifetime is the caller's business: link it to a supervised
process to keep it, or let it die with the process that made it.
Rows are tagged tuples in one :set, so every read is a key lookup:
{{:canonical, spec_id}, binary} # the evidence, shared by versions
{{:operation, ref, op_id}, %Operation{}}
{{:component, ref, kind, name}, json}
{{:index, ref}, [%OperationSummary{}]}
{{:meta, ref}, %Meta{}} # written last: the commit markerspec_id is the content hash itself — nothing else is needed to name a
document, and it makes put/2 idempotent without a read-modify-write.
put/2 writes in the order listed — canonical bytes, then the operation
and component rows, then the index, and {:meta, ref} last — and all five
reads gate on {:meta, ref} first, so a projection mid-write is invisible
rather than half-visible, and a concurrent putter that finds the marker
knows the rest is already there. Every write is the same bytes at the same
key, so racing putters cannot interleave into something neither of them
wrote.
pointer/4 decodes the canonical document per call. That is the slow path
by design (SpecStore's docs), and a store meant for small specs should
not hold a decoded copy of every document it has ever seen.
Summary
Functions
Deletes one projection's rows, and the canonical bytes if it was the last version holding them.
Creates a store table and returns it — the handle a store tuple carries.
Functions
@spec delete_projection(:ets.table(), OapiCodemode.SpecStore.ref()) :: :ok
Deletes one projection's rows, and the canonical bytes if it was the last version holding them.
Not an OapiCodemode.SpecStore callback, and deliberately not: a store is
append-only in the version dimension precisely so a ref handed out earlier
keeps reading what it always read, and a host's own store owns its own
retention. This exists for the one store whose lifetime the library owns —
OapiCodemode.Registry's private table, where re-registering an api name
through OapiCodemode.ingest_and_register/4 supersedes the projection that
name held and nothing would otherwise ever free it.
So the append-only promise holds for every store but that one, and there
it is narrower than it sounds: the registry checks that no live binding
names the ref, not that nobody is mid-read against it. A run that took its
ref before a concurrent re-registration keeps reading a projection that can
be deleted underneath it — every read then answers {:error, :not_found}
({:meta, ref} goes first, below), which surfaces as a describe error
payload or the proxy's :resolve phase. A stale read, never a wrong one:
no other document's rows can appear under a ref this one wrote. Deleting a
ref a registration still names is the caller's mistake to avoid.
{:meta, ref} goes first, so a concurrent reader sees :not_found rather
than half a projection — put/2's write order, reversed.
@spec new() :: :ets.table()
Creates a store table and returns it — the handle a store tuple carries.