# `Runic.Runner.Store`
[🔗](https://github.com/zblanco/runic/blob/main/lib/runic/runner/store.ex#L1)

Behaviour for workflow persistence adapters.

Adapters handle saving and loading workflow event logs for
durability across process restarts.

## Stream Semantics (Event-Sourced)

The preferred interface uses `append/3` and `stream/2` for incremental
event persistence. Events are appended after each execution cycle and
streamed on recovery to rebuild workflow state via `Workflow.from_events/1`.

Stores that implement `append/3` and `stream/2` get automatic
event-sourced checkpointing and recovery from the Worker.

`stream/2` must always return the full event stream for a workflow. Stores
that support cursor-aware or windowed replay can additionally implement
`stream/3` with options.

Supported `stream/3` options:

  * `:after_cursor` — exclusive lower bound. `after_cursor: 10` returns
    events with sequence/cursor greater than `10`.
  * `:limit` — optional maximum number of events to return. Adapters may
    ignore this when their backing stream does not support bounded reads.
  * `:batch_size` — optional page-size hint for stores that fetch event rows
    in batches.

`stream/3` should be a superset of `stream/2`: calling it with an empty
option list should return the full stream. Adapters should ignore unknown
options they do not support.

## Legacy Semantics (Snapshot)

The `save/3` and `load/2` callbacks persist the full workflow log as a
snapshot. These remain the required baseline interface for backward
compatibility. Stores that only implement `save/load` continue to work
unchanged.

## Optional Capabilities

- **Snapshots** (`save_snapshot/4`, `load_snapshot/2`): Point-in-time
  workflow snapshots for faster recovery (replay from snapshot + events
  after cursor instead of full replay).
- **Fact storage** (`save_fact/3`, `load_fact/2`): Content-addressed fact
  value storage for hybrid rehydration without loading all values into memory.
- **Payload storage** (`save_payload/3`, `load_payload/2`): Immutable encoded
  payload bytes keyed by a `:payload` `Runic.Identity`.

# `cursor`

```elixir
@type cursor() :: non_neg_integer()
```

# `event`

```elixir
@type event() :: struct()
```

# `log`

```elixir
@type log() :: [struct()]
```

# `state`

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

# `stream_opts`

```elixir
@type stream_opts() :: [
  after_cursor: cursor(),
  limit: pos_integer(),
  batch_size: pos_integer()
]
```

# `workflow_id`

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

# `append`
*optional* 

```elixir
@callback append(workflow_id(), events :: [event()], state()) ::
  {:ok, cursor()} | {:error, term()}
```

# `checkpoint`
*optional* 

```elixir
@callback checkpoint(workflow_id(), log(), state()) :: :ok | {:error, term()}
```

# `delete`
*optional* 

```elixir
@callback delete(workflow_id(), state()) :: :ok | {:error, term()}
```

# `exists?`
*optional* 

```elixir
@callback exists?(workflow_id(), state()) :: boolean()
```

# `init_store`

```elixir
@callback init_store(opts :: keyword()) :: {:ok, state()} | {:error, term()}
```

# `list`
*optional* 

```elixir
@callback list(state()) :: {:ok, [workflow_id()]} | {:error, term()}
```

# `load`

```elixir
@callback load(workflow_id(), state()) :: {:ok, log()} | {:error, :not_found | term()}
```

# `load_fact`
*optional* 

```elixir
@callback load_fact(fact_hash :: term(), state()) ::
  {:ok, term()} | {:error, :not_found | term()}
```

# `load_payload`
*optional* 

```elixir
@callback load_payload(Runic.Identity.t(), state()) ::
  {:ok, binary()} | {:error, :not_found | term()}
```

# `load_snapshot`
*optional* 

```elixir
@callback load_snapshot(workflow_id(), state()) ::
  {:ok, {cursor(), binary()}} | {:error, :not_found | term()}
```

# `save`

```elixir
@callback save(workflow_id(), log(), state()) :: :ok | {:error, term()}
```

# `save_fact`
*optional* 

```elixir
@callback save_fact(fact_hash :: term(), value :: term(), state()) ::
  :ok | {:error, term()}
```

# `save_payload`
*optional* 

```elixir
@callback save_payload(Runic.Identity.t(), encoded_payload :: binary(), state()) ::
  :ok | {:error, term()}
```

# `save_snapshot`
*optional* 

```elixir
@callback save_snapshot(workflow_id(), cursor(), snapshot :: binary(), state()) ::
  :ok | {:error, term()}
```

# `stream`
*optional* 

```elixir
@callback stream(workflow_id(), state()) ::
  {:ok, Enumerable.t()} | {:error, :not_found | term()}
```

# `stream`
*optional* 

```elixir
@callback stream(workflow_id(), state(), stream_opts()) ::
  {:ok, Enumerable.t()} | {:error, :not_found | term()}
```

# `supports_snapshots?`

```elixir
@spec supports_snapshots?(module()) :: boolean()
```

Returns true if the store module supports snapshot save/load semantics.

# `supports_stream?`

```elixir
@spec supports_stream?(module()) :: boolean()
```

Returns true if the store module supports event-sourced stream semantics.

# `supports_stream_options?`

```elixir
@spec supports_stream_options?(module()) :: boolean()
```

Returns true if the store module supports option-aware stream replay.

---

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