# `Electric.Client.ShapeState`
[🔗](https://github.com/electric-sql/electric/tree/%40core/elixir-client%400.9.4/packages/elixir-client/lib/electric/client/shape_state.ex#L1)

State for polling a shape.

This struct holds the state needed between polling requests, including:
- The shape handle and offset for resuming
- Schema and value mapper for parsing responses
- Tag tracking data for generating synthetic deletes from move-out events

## Usage

    # Create initial state
    state = ShapeState.new()

    # Poll for changes
    {:ok, messages, new_state} = Client.poll(client, shape, state)

    # State can also be created from a ResumeMessage (interop with stream API)
    state = ShapeState.from_resume(resume_message)

# `t`

```elixir
@type t() :: %Electric.Client.ShapeState{
  fast_loop_consecutive_count: non_neg_integer(),
  key_data: %{optional(term()) =&gt; %{tags: MapSet.t(), msg: term()}},
  next_cursor: Electric.Client.cursor() | nil,
  offset: Electric.Client.Offset.t(),
  recent_requests: [{integer(), Electric.Client.Offset.t()}],
  schema: Electric.Client.schema() | nil,
  shape_handle: Electric.Client.shape_handle() | nil,
  stale_cache_buster: String.t() | nil,
  stale_cache_retry_count: non_neg_integer(),
  tag_to_keys: %{optional(term()) =&gt; MapSet.t()},
  up_to_date?: boolean(),
  value_mapper_fun: Electric.Client.ValueMapper.mapper_fun() | nil
}
```

# `check_fast_loop`

```elixir
@spec check_fast_loop(t()) ::
  {:ok, t()} | {:backoff, non_neg_integer(), t()} | {:error, String.t()}
```

Check for fast-loop condition on non-live requests.

Tracks recent requests in a sliding window. If too many requests occur at
the same offset within the window, the client is stuck in a retry loop.

Returns:
  * `{:ok, state}` — no fast loop detected
  * `{:backoff, ms, state}` — fast loop detected, caller should sleep `ms`
  * `{:error, message}` — fast loop persisted beyond max retries

# `clear_fast_loop`

```elixir
@spec clear_fast_loop(t()) :: t()
```

Clear fast-loop tracking state.

Called when the client transitions to live mode (up-to-date), since
rapid polling is expected behaviour in live mode.

# `clear_stale_retry`

```elixir
@spec clear_stale_retry(t()) :: t()
```

Clear stale retry state after a successful response.

Called when we receive a fresh (non-stale) response from the server.

# `enter_stale_retry`

```elixir
@spec enter_stale_retry(t()) :: t()
```

Enter stale retry mode by setting a cache buster and incrementing the retry count.

Called when a stale CDN response is detected - the server returns an expired
handle that matches our cached expired handle.

# `from_resume`

```elixir
@spec from_resume(Electric.Client.Message.ResumeMessage.t()) :: t()
```

Create polling state from a ResumeMessage.

This allows interop between the streaming and polling APIs - you can
use `live: false` to get a ResumeMessage from a stream, then continue
polling from that point.

# `generate_cache_buster`

```elixir
@spec generate_cache_buster() :: String.t()
```

Generate a random cache buster string.

Uses 8 random bytes encoded as hex (16 characters).

# `new`

```elixir
@spec new(keyword()) :: t()
```

Create a new initial polling state.

## Options

  * `:shape_handle` - Optional shape handle to resume from
  * `:offset` - Optional offset to resume from (default: before_all)
  * `:schema` - Optional schema for value mapping

# `reset`

```elixir
@spec reset(t(), Electric.Client.shape_handle()) :: t()
```

Reset polling state for a new shape handle, preserving schema and value mapper.

Used when a 409 (must-refetch) response is received — the shape handle changes
but the schema remains the same.

# `to_resume`

```elixir
@spec to_resume(t()) :: Electric.Client.Message.ResumeMessage.t()
```

Convert polling state to a ResumeMessage for use with the streaming API.

---

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