Electric.Client.TagTracker (Electric Client v0.10.2)

Copy Markdown View Source

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).

Summary

Functions

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

Activate positions for keys matching move-in patterns.

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

Evaluate DNF visibility from active_conditions and disjunct structure.

Update the tag index when a change message is received.

Types

disjunct_positions()

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

key()

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

key_data()

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

position_hash()

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

tag_to_keys()

@type tag_to_keys() :: %{optional(position_hash()) => MapSet.t(key())}

Functions

generate_synthetic_deletes(tag_to_keys, key_data, disjunct_positions, patterns, request_timestamp)

@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(tag_to_keys, key_data, patterns)

@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(tags)

@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?(active_conditions, disjunct_positions)

@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(tag_to_keys, key_data, disjunct_positions, msg)

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}.