# `Snodo.Extensions.Tasks.Store`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.2.0/extensions/tasks/lib/snodo/extensions/tasks/store.ex#L1)

Application-owned persistence contract for the Tasks extension.

A request context crosses this boundary only through `authorize/3`. The
resulting access value is opaque and deliberately scoped to one action. All
mutations after that point are explicit, JSON-safe events guarded by an
expected snapshot revision.

Creation atomically persists the initial Task and its serializable work
descriptor. Runners then acquire renewable, opaque claims. A lease can
authorize only worker lifecycle events for its exact task and claim
generation; it is not a substitute for fresh request authorization on
`tasks/get`, `tasks/update`, or `tasks/cancel`.

`claim_next/3` is the recovery seam. Expired or adapter-invalidated claims may
be reacquired at a higher generation, fencing the previous worker. Both exact
and recovery claims must also enforce the snapshot's persisted `retry_at`
against store-authoritative time. Reaping removes the entire aggregate using
the Task's creation-based TTL.

Store references use `{module, state}` so implementations may be processes,
database repositories, external job systems, or immutable test doubles.

# `access`

```elixir
@type access() :: term()
```

# `action`

```elixir
@type action() ::
  {:create, String.t()}
  | {:get, String.t()}
  | {:update, String.t()}
  | {:cancel, String.t()}
```

# `authority`

```elixir
@type authority() :: {:request, access()} | {:worker, worker_lease()}
```

# `claim_next_result`

```elixir
@type claim_next_result() ::
  {:ok, Snodo.Extensions.Tasks.Snapshot.t(), worker_lease()}
  | :empty
  | {:error, term()}
```

# `claim_result`

```elixir
@type claim_result() ::
  {:ok, Snodo.Extensions.Tasks.Snapshot.t(), worker_lease()}
  | {:deferred, pos_integer()}
  | :unavailable
  | :not_found
  | {:error, term()}
```

# `lookup_result`

```elixir
@type lookup_result() ::
  {:ok, Snodo.Extensions.Tasks.Snapshot.t()} | :not_found | {:error, term()}
```

# `ref`

```elixir
@type ref() :: {module(), term()}
```

# `transition_result`

```elixir
@type transition_result() ::
  {:ok, Snodo.Extensions.Tasks.Transition.t()}
  | {:conflict, Snodo.Extensions.Tasks.Snapshot.t()}
  | :not_found
  | {:error, term()}
```

# `worker_lease`

```elixir
@type worker_lease() :: term()
```

# `authorize`

```elixir
@callback authorize(store :: term(), Snodo.Context.t(), action()) ::
  {:ok, access()} | {:error, term()}
```

# `claim`

```elixir
@callback claim(
  store :: term(),
  task_id :: String.t(),
  owner_id :: String.t(),
  lease_ms :: pos_integer()
) ::
  claim_result()
```

# `claim_next`

```elixir
@callback claim_next(store :: term(), owner_id :: String.t(), lease_ms :: pos_integer()) ::
  claim_next_result()
```

# `create`

```elixir
@callback create(
  store :: term(),
  Snodo.Extensions.Tasks.Task.t(),
  Snodo.Extensions.Tasks.Work.t(),
  access()
) :: {:ok, Snodo.Extensions.Tasks.Snapshot.t()} | {:error, term()}
```

# `get`

```elixir
@callback get(store :: term(), task_id :: String.t(), access()) :: lookup_result()
```

# `reap`

```elixir
@callback reap(store :: term()) :: {:ok, [String.t()]} | {:error, term()}
```

# `release`

```elixir
@callback release(store :: term(), worker_lease()) :: :ok | {:error, term()}
```

# `renew`

```elixir
@callback renew(store :: term(), worker_lease(), lease_ms :: pos_integer()) ::
  {:ok, worker_lease()} | {:error, term()}
```

# `transition`

```elixir
@callback transition(
  store :: term(),
  task_id :: String.t(),
  expected_revision :: non_neg_integer(),
  Snodo.Extensions.Tasks.Event.t(),
  authority()
) :: transition_result()
```

# `worker_snapshot`

```elixir
@callback worker_snapshot(store :: term(), task_id :: String.t(), worker_lease()) ::
  lookup_result()
```

# `authorize`

```elixir
@spec authorize(ref(), Snodo.Context.t(), action()) ::
  {:ok, access()} | {:error, term()}
```

Acquires opaque, action-bound request access without retaining the context.

# `claim`

```elixir
@spec claim(ref(), String.t(), String.t(), pos_integer()) :: claim_result()
```

Atomically claims one exact nonterminal task whose retry delay is due.

# `claim_next`

```elixir
@spec claim_next(ref(), String.t(), pos_integer()) :: claim_next_result()
```

Atomically claims the next available, retry-due recoverable task.

# `create`

```elixir
@spec create(
  ref(),
  Snodo.Extensions.Tasks.Task.t(),
  Snodo.Extensions.Tasks.Work.t(),
  access()
) ::
  {:ok, Snodo.Extensions.Tasks.Snapshot.t()} | {:error, term()}
```

# `get`

```elixir
@spec get(ref(), String.t(), access()) :: lookup_result()
```

# `reap`

```elixir
@spec reap(ref()) :: {:ok, [String.t()]} | {:error, term()}
```

Deletes complete task aggregates whose creation-based TTL has elapsed.

# `release`

```elixir
@spec release(ref(), worker_lease()) :: :ok | {:error, term()}
```

Releases a matching claim so another runner may recover it immediately.

# `renew`

```elixir
@spec renew(ref(), worker_lease(), pos_integer()) ::
  {:ok, worker_lease()} | {:error, term()}
```

Extends a live worker claim without changing task state or revision.

# `transition`

```elixir
@spec transition(
  ref(),
  String.t(),
  non_neg_integer(),
  Snodo.Extensions.Tasks.Event.t(),
  authority()
) :: transition_result()
```

# `validate_ref!`

```elixir
@spec validate_ref!(term()) :: ref()
```

# `worker_snapshot`

```elixir
@spec worker_snapshot(ref(), String.t(), worker_lease()) :: lookup_result()
```

Reads the current aggregate through an exact, live worker lease.

---

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