# `Exgit.ObjectStore.SharedPromisor`
[🔗](https://github.com/ivarvong/exgit/blob/v0.1.0/lib/exgit/object_store/shared_promisor.ex#L1)

GenServer wrapper around `Exgit.ObjectStore.Promisor` that
serializes cache access across processes.

The plain `Promisor` is a pure value: two concurrent
`resolve(p, sha_a)` / `resolve(p, sha_b)` calls from the same `p`
each fetch independently and only one caller's grown cache
survives — a benign but wasteful **cache race**.

For workloads that genuinely do concurrent bulk reads against the
same repo (a grep agent spawning N tasks, a CI worker hydrating a
cache from many branches in parallel), wrap the Promisor in a
SharedPromisor:

    {:ok, pid} = SharedPromisor.start_link(promisor)

    # Both tasks see + grow the same cache, serialized by the
    # GenServer. No race, no duplicate fetches.
    Task.async(fn -> SharedPromisor.resolve(pid, sha_a) end)
    Task.async(fn -> SharedPromisor.resolve(pid, sha_b) end)

The API mirrors `Promisor.resolve/2` / `put/2` / `empty?/1` /
`fetch_with_filter/3` — each call acquires a GenServer lock,
mutates the internal Promisor, and releases.

## When NOT to use this

  * Single-threaded agent loops. The plain pure-value Promisor is
    faster (no message-passing overhead).
  * Short-lived scripts. The setup cost of spinning up a
    GenServer is ~200µs, usually more than the work saved.
  * Repos you'd rather snapshot. Share the `%Promisor{}` struct
    by value; two tasks holding the same struct see the same
    cache deterministically.

## Failure semantics

If the wrapped Promisor call raises (e.g. a transport
misconfiguration), the exception propagates through to the
caller and the GenServer terminates. Supervise it under a
`:one_for_one` strategy in your app tree if you want
auto-restart; callers will need to re-discover the new pid and
re-seed if they want state to persist across restarts.

## Telemetry

Each public call emits a `[:exgit, :object_store, :shared_promisor, op]`
span event where `op` is `:resolve | :put | :has? | :get`. Use this
to track lock-contention latency separately from the underlying
`[:exgit, :object_store, :*]` Promisor events.

# `server`

```elixir
@type server() :: GenServer.server()
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `empty?`

```elixir
@spec empty?(server(), timeout()) :: boolean()
```

True if the wrapped cache has no objects.

# `fetch_with_filter`

```elixir
@spec fetch_with_filter(server(), [binary()], keyword(), timeout()) ::
  :ok | {:error, term()}
```

Fetch with an explicit filter spec and merge results into the
cache. See `Promisor.fetch_with_filter/3`.

# `get`

```elixir
@spec get(server(), binary(), timeout()) :: {:ok, Exgit.Object.t()} | {:error, term()}
```

Pure-read lookup. Does NOT grow the cache; returns `{:error, :not_found}` on miss.

# `has_object?`

```elixir
@spec has_object?(server(), binary(), timeout()) :: boolean()
```

True if `sha` is in the local cache. Does NOT trigger a fetch.

# `put`

```elixir
@spec put(server(), Exgit.Object.t(), timeout()) ::
  {:ok, binary()} | {:error, :cache_overfull}
```

Insert an object into the shared cache. Returns `{:ok, sha}` or
`{:error, :cache_overfull}` when the wrapped Promisor has
`:on_overfull => :error`.

# `replace`

```elixir
@spec replace(server(), Exgit.ObjectStore.Promisor.t(), timeout()) :: :ok
```

Replace the wrapped Promisor. Use sparingly — this discards the
current cache. Intended for testing and for swapping transports.

# `resolve`

```elixir
@spec resolve(server(), binary(), timeout()) ::
  {:ok, Exgit.Object.t()} | {:error, term()}
```

Look up `sha`, fetching from the transport on cache miss.

Unlike `Promisor.resolve/2`, does NOT return a promisor in the
success tuple — the cache grew inside the GenServer, and the
next call will see the updated state automatically.

Returns one of:

  * `{:ok, object}` — cache hit or successful fetch
  * `{:error, reason}` — transport failure, cache unchanged
  * `{:error, :not_found}` — fetch returned a pack that didn't
    contain the requested SHA; sibling objects that WERE
    returned are now in the cache and will benefit future calls.

# `snapshot`

```elixir
@spec snapshot(server(), timeout()) :: Exgit.ObjectStore.Promisor.t()
```

Snapshot the current Promisor state. Useful for serialization,
debugging, or handing back a pure value after the shared cache
has served its purpose.

# `start_link`

```elixir
@spec start_link(Exgit.ObjectStore.Promisor.t(), GenServer.options()) ::
  GenServer.on_start()
```

Start a SharedPromisor process wrapping the given Promisor.

Accepts all the usual `GenServer.start_link/3` options (`:name`,
`:timeout`, etc.) plus a required `promisor` key.

## Examples

    {:ok, pid} = SharedPromisor.start_link(my_promisor)
    {:ok, pid} = SharedPromisor.start_link(my_promisor, name: MyApp.Cache)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
