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

Manages tag tracking for move-out support in Electric shapes.

This module handles tracking which keys have which tags, enabling the
generation of synthetic delete messages when rows move out of a shape's
subquery filter.

## Data Structures

Three structures are maintained:
- `tag_to_keys`: `%{{position, hash} => MapSet<key>}` - which keys have each position-hash pair
- `key_data`: `%{key => %{tags: MapSet<{pos, hash}>, active_conditions: [boolean()] | nil, msg: msg}}` - each key's current state
- `disjunct_positions`: `[[integer()]] | nil` - shared across all keys, derived once from the first tagged message

Tags arrive as slash-delimited strings per disjunct (e.g., `"hash1/hash2/"`, `"//hash3"`).
They are normalized into 2D arrays and indexed by `{position, hash_value}` tuples.

For shapes with `active_conditions`, visibility is evaluated using DNF (Disjunctive Normal Form):
a row is visible if at least one disjunct is satisfied (OR of ANDs over positions).

# `disjunct_positions`

```elixir
@type disjunct_positions() :: [[non_neg_integer()]] | nil
```

# `key`

```elixir
@type key() :: String.t()
```

# `key_data`

```elixir
@type key_data() :: %{
  optional(key()) =&gt; %{
    tags: MapSet.t(position_hash()),
    active_conditions: [boolean()] | nil,
    msg: Electric.Client.Message.ChangeMessage.t()
  }
}
```

# `position_hash`

```elixir
@type position_hash() :: {non_neg_integer(), String.t()}
```

# `tag_to_keys`

```elixir
@type tag_to_keys() :: %{optional(position_hash()) =&gt; MapSet.t(key())}
```

# `generate_synthetic_deletes`

```elixir
@spec generate_synthetic_deletes(
  tag_to_keys(),
  key_data(),
  disjunct_positions(),
  [map()],
  DateTime.t()
) :: {[Electric.Client.Message.ChangeMessage.t()], tag_to_keys(), key_data()}
```

Generate synthetic delete messages for keys matching move-out patterns.

Patterns contain `%{pos: position, value: hash}` maps. For keys with
`active_conditions`, positions are deactivated and visibility is re-evaluated
using DNF with the shared `disjunct_positions`. For keys without
`active_conditions`, the old behavior applies: delete when no entries remain.

Returns `{synthetic_deletes, updated_tag_to_keys, updated_key_data}`.

# `handle_move_in`

```elixir
@spec handle_move_in(tag_to_keys(), key_data(), [map()]) ::
  {tag_to_keys(), key_data()}
```

Activate positions for keys matching move-in patterns.

Sets `active_conditions[pos]` to `true` for keys that have
matching `{pos, value}` entries in the tag index.

Returns `{updated_tag_to_keys, updated_key_data}`.

# `normalize_tags`

```elixir
@spec normalize_tags([String.t()]) :: [[String.t() | nil]]
```

Normalize slash-delimited wire format tags to 2D arrays.

Each tag string represents a disjunct with "/" separating position hashes.
Empty strings are replaced with nil (position not relevant to this disjunct).

## Examples

    iex> Electric.Client.TagTracker.normalize_tags(["hash_a/hash_b"])
    [["hash_a", "hash_b"]]

    iex> Electric.Client.TagTracker.normalize_tags(["hash_a/", "/hash_b"])
    [["hash_a", nil], [nil, "hash_b"]]

    iex> Electric.Client.TagTracker.normalize_tags(["tag_a"])
    [["tag_a"]]

# `row_visible?`

```elixir
@spec row_visible?([boolean()], [[non_neg_integer()]]) :: boolean()
```

Evaluate DNF visibility from active_conditions and disjunct structure.

A row is visible if at least one disjunct is satisfied.
A disjunct is satisfied when all its positions have `active_conditions[pos] == true`.

# `update_tag_index`

```elixir
@spec update_tag_index(
  tag_to_keys(),
  key_data(),
  disjunct_positions(),
  Electric.Client.Message.ChangeMessage.t()
) :: {tag_to_keys(), key_data(), disjunct_positions()}
```

Update the tag index when a change message is received.

Tags are normalized from slash-delimited wire format to position-indexed entries.
`disjunct_positions` is derived once from the first tagged message and reused for all
subsequent messages, since it is determined by the shape's WHERE clause structure.

Returns `{updated_tag_to_keys, updated_key_data, disjunct_positions}`.

---

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