Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
pulp::state::StateTree Class Reference

Reactive tree node — each node has a type name, properties, and children. More...

#include <state_tree.hpp>

Classes

class  Transaction
 RAII transaction on a StateTree. More...
class  ValidatedTransaction
 A transaction whose edits are checked and return TreeEditResult instead of being silently ignored. More...
class  SyncedClone
 Owns the listener wiring created by StateTree::clone_synced(). More...

Public Types

using Ptr = std::shared_ptr<StateTree>

Public Member Functions

const std::string & type_name () const
 Type name (e.g., "preset", "oscillator", "filter").
void set (std::string_view name, PropertyValue value)
 Set a property value (notifies listeners).
TreeEditResult set_checked (std::string_view name, PropertyValue value, const PropertyValidator &validator={})
 Set a property, returning an error value instead of applying an invalid edit: an empty name, or a value validator refuses.
PropertyValue get (std::string_view name) const
 Get a property value (returns monostate if not set).
bool get_bool (std::string_view name, bool default_val=false) const
 Typed getters with defaults.
int64_t get_int (std::string_view name, int64_t default_val=0) const
double get_double (std::string_view name, double default_val=0.0) const
std::string get_string (std::string_view name, std::string_view default_val="") const
bool has (std::string_view name) const
 Whether a property exists.
void remove (std::string_view name)
 Remove a property.
TreeEditResult remove_checked (std::string_view name)
 Remove a property; kMissingProperty if it does not exist.
std::vector< std::string > property_names () const
 Get all property names (snapshot copy).
void add_child (Ptr child)
 Add a child node (notifies child-added listeners).
TreeEditResult add_child_checked (Ptr child)
 Checked add_child(): returns an error instead of ignoring the edit.
void insert_child (int index, Ptr child)
 Insert a child at a specific index (clamped to the valid range).
TreeEditResult insert_child_checked (int index, Ptr child)
 Checked insert_child(): an out-of-range index is an error, not clamped.
void remove_child (int index)
 Remove a child by index (notifies child-removed listeners).
TreeEditResult remove_child_checked (int index)
 Checked remove_child(int).
void remove_child (StateTree *child)
 Remove a specific child.
void remove_all_children ()
 Remove every child.
int child_count () const
 Number of children.
Ptr child (int index) const
 Get child by index.
int index_of (const StateTree *node) const
 Index of node among this node's children, or -1.
Ptr find_child (std::string_view type_name) const
 Find first child with matching type name.
std::vector< Ptr > find_children (std::string_view type_name) const
 Find all children with matching type name.
void for_each_descendant (const std::function< void(StateTree &)> &fn)
 Visit every descendant (not this node) depth-first, parents before children.
Ptr find_descendant (const std::function< bool(const StateTree &)> &predicate) const
 First descendant (depth-first, parents before children, not this node) for which predicate returns true, or null.
bool is_equivalent_to (const StateTree &other) const
 Structural equality: same type name, equal properties (exact variant match, so int64 3 differs from double 3.0) and equivalent children in the same order.
StateTree * parent () const
 Parent (may be null for root).
Transaction begin_transaction ()
 Open (or join) a transaction on this node's tree.
Transaction begin_transaction (UndoManager *undo, std::string_view name)
 Open (or join) a transaction that records inverse edits so the whole transaction becomes one named step in undo when the outermost transaction commits.
ValidatedTransaction begin_validated_transaction (PropertyValidator validator={}, UndoManager *undo=nullptr, std::string_view name={})
 Open (or join) a transaction whose edits are checked and return TreeEditResult.
bool in_transaction () const
 Whether a transaction is open that covers edits made through this node.
int add_listener (TreeListener listener, ListenerDelivery delivery=ListenerDelivery::kCoalesced)
 Listen for property changes.
void remove_listener (int id)
int add_child_added_listener (ChildListener listener, ListenerDelivery delivery=ListenerDelivery::kCoalesced)
 Listen for child added/removed.
int add_child_removed_listener (ChildListener listener, ListenerDelivery delivery=ListenerDelivery::kCoalesced)
void remove_child_added_listener (int id)
void remove_child_removed_listener (int id)
int add_child_moved_listener (ChildMovedListener listener, ListenerDelivery delivery=ListenerDelivery::kCoalesced)
 Listen for reorders within this node (move_child with one parent).
void remove_child_moved_listener (int id)
int add_parent_changed_listener (ParentChangedListener listener, ListenerDelivery delivery=ListenerDelivery::kCoalesced)
 Listen for this node's parent changing.
void remove_parent_changed_listener (int id)
int add_ancestor_changed_listener (AncestorChangedListener listener, ListenerDelivery delivery=ListenerDelivery::kCoalesced)
 Listen for any ancestor link above this node changing.
void remove_ancestor_changed_listener (int id)
std::string to_json () const
 Serialize to JSON string.
Ptr deep_copy () const
 Deep-copy this subtree (including properties and recursive children).
SyncedClone clone_synced ()

Static Public Member Functions

static Ptr create (std::string type_name)
 Create a tree node with a type name.
static bool move_child (StateTree &from_parent, int from_index, StateTree &to_parent, int to_index)
 Move the child at from_index of from_parent to to_index of to_parent.
static TreeEditResult move_child_checked (StateTree &from_parent, int from_index, StateTree &to_parent, int to_index)
 Checked move_child().
static Ptr from_json (std::string_view json)
 Deserialize from JSON string.

Detailed Description

Reactive tree node — each node has a type name, properties, and children.

Thread-safety contract: StateTree is not thread-safe. Every public method below — including const accessors like get() and property_names() — touches non-atomic members (properties_, children_, listener vectors) without internal locking. Callers must serialize all access from a single thread, or provide their own external mutex.

The intended usage model is "main-thread / UI-thread only" (audio threads should use the lock-free StateStore for parameter exchange instead). The <mutex> header is included as a forward-compatibility hook for callers that wrap an external std::scoped_lock around a StateTree handle; the class itself holds none. Transactions follow the same rule: begin, commit and abort on the thread that owns the tree.

Mutating a StateTree from one thread while another thread reads it is a data race even when only "harmless"-looking const methods are involved (e.g. property_names() copies a std::map while a writer may rehash it). The TSan regression in test_state_tree.cpp pins this contract.

Listener contract:

  • Listeners run after the edit is applied, so they observe the final state of that edit: during a move, the child_removed callback on the old parent already sees child.parent() as the new parent.
  • A move to a different parent notifies child_removed on the old parent, then child_added on the new parent, then parent_changed on the child, then ancestor_changed on the child and every descendant. A reorder within one parent via move_child() notifies child_moved.
  • remove_all_children() detaches every child first, then notifies from the last child backwards, so each child_removed index is valid against a list that is shrinking from the end.
  • Inside a transaction, coalesced listeners receive one burst on commit. The burst preserves edit order, merges repeated changes to the same property of the same node into one notification (first old value, last new value) and drops property changes whose net effect is nil. Structural notifications are delivered one per edit, with indices as they were when the edit was made, while the tree is already in its final state.

Transactions and reentrancy:

  • begin_transaction() opens a transaction on the topmost ancestor of the node, so it covers edits made through any node of that tree. A transaction begun while one is already open on the tree joins it; the burst is delivered when the last open Transaction ends.
  • Edits are applied immediately; abort() reverts the edits made since that Transaction began, in reverse order, and drops their pending notifications. A subtree removed during the transaction stays part of it until it is re-attached elsewhere, so abort can restore it.
  • A listener running inside a commit burst may edit the tree directly (those edits notify inline) but must not begin a transaction on the same tree: such a Transaction is inert (is_active() is false, edits are not grouped and abort() does nothing), and a ValidatedTransaction begun there refuses every edit with kReentrantTransaction.
  • Do not call UndoManager::undo() / redo() for a tree step while a transaction is open on that tree; the replayed edits would join it.

Member Typedef Documentation

◆ Ptr

using pulp::state::StateTree::Ptr = std::shared_ptr<StateTree>

Member Function Documentation

◆ add_ancestor_changed_listener()

int pulp::state::StateTree::add_ancestor_changed_listener ( AncestorChangedListener listener,
ListenerDelivery delivery = ListenerDelivery::kCoalesced )

Listen for any ancestor link above this node changing.

NOT thread-safe — caller must serialize.

◆ add_child()

void pulp::state::StateTree::add_child ( Ptr child)

Add a child node (notifies child-added listeners).

A child that already has a parent is moved here (child_removed on the old parent, then child_added here, then parent_changed). Edits that would create a cycle, or span two trees with different open transactions, are ignored; use add_child_checked() to get the reason. NOT thread-safe — caller must serialize. Mutates children_ and the child's parent_ back-pointer.

◆ add_child_added_listener()

int pulp::state::StateTree::add_child_added_listener ( ChildListener listener,
ListenerDelivery delivery = ListenerDelivery::kCoalesced )

Listen for child added/removed.

NOT thread-safe — caller must serialize.

◆ add_child_checked()

TreeEditResult pulp::state::StateTree::add_child_checked ( Ptr child)

Checked add_child(): returns an error instead of ignoring the edit.

NOT thread-safe — message thread only.

◆ add_child_moved_listener()

int pulp::state::StateTree::add_child_moved_listener ( ChildMovedListener listener,
ListenerDelivery delivery = ListenerDelivery::kCoalesced )

Listen for reorders within this node (move_child with one parent).

NOT thread-safe — caller must serialize.

◆ add_child_removed_listener()

int pulp::state::StateTree::add_child_removed_listener ( ChildListener listener,
ListenerDelivery delivery = ListenerDelivery::kCoalesced )

◆ add_listener()

int pulp::state::StateTree::add_listener ( TreeListener listener,
ListenerDelivery delivery = ListenerDelivery::kCoalesced )

Listen for property changes.

NOT thread-safe — caller must serialize. Listener vectors are not guarded; registering or removing while a notify is in flight on another thread is a data race.

◆ add_parent_changed_listener()

int pulp::state::StateTree::add_parent_changed_listener ( ParentChangedListener listener,
ListenerDelivery delivery = ListenerDelivery::kCoalesced )

Listen for this node's parent changing.

NOT thread-safe — caller must serialize.

◆ begin_transaction() [1/2]

Transaction pulp::state::StateTree::begin_transaction ( )
nodiscard

Open (or join) a transaction on this node's tree.

See the class comment for the full contract. NOT thread-safe — message thread only.

◆ begin_transaction() [2/2]

Transaction pulp::state::StateTree::begin_transaction ( UndoManager * undo,
std::string_view name )
nodiscard

Open (or join) a transaction that records inverse edits so the whole transaction becomes one named step in undo when the outermost transaction commits.

The first manager supplied while the transaction is open wins; a later one passed by a joining transaction is ignored. An aborted or empty transaction pushes no step. NOT thread-safe — message thread only; undo must outlive the commit.

◆ begin_validated_transaction()

ValidatedTransaction pulp::state::StateTree::begin_validated_transaction ( PropertyValidator validator = {},
UndoManager * undo = nullptr,
std::string_view name = {} )
nodiscard

Open (or join) a transaction whose edits are checked and return TreeEditResult.

validator (optional) vets every property value. NOT thread-safe — message thread only.

◆ child()

Ptr pulp::state::StateTree::child ( int index) const

Get child by index.

NOT thread-safe — caller must serialize.

◆ child_count()

int pulp::state::StateTree::child_count ( ) const
inline

Number of children.

NOT thread-safe — caller must serialize. std::vector::size is non- atomic relative to insert/remove.

◆ clone_synced()

SyncedClone pulp::state::StateTree::clone_synced ( )

◆ create()

Ptr pulp::state::StateTree::create ( std::string type_name)
inlinestatic

Create a tree node with a type name.

Thread-safe (only touches the local arguments).

◆ deep_copy()

Ptr pulp::state::StateTree::deep_copy ( ) const

Deep-copy this subtree (including properties and recursive children).

NOT thread-safe — caller must serialize with any concurrent mutator of *this. The returned copy is itself a fresh tree.

◆ find_child()

Ptr pulp::state::StateTree::find_child ( std::string_view type_name) const

Find first child with matching type name.

NOT thread-safe — caller must serialize.

◆ find_children()

std::vector< Ptr > pulp::state::StateTree::find_children ( std::string_view type_name) const

Find all children with matching type name.

NOT thread-safe — caller must serialize.

◆ find_descendant()

Ptr pulp::state::StateTree::find_descendant ( const std::function< bool(const StateTree &)> & predicate) const

First descendant (depth-first, parents before children, not this node) for which predicate returns true, or null.

NOT thread-safe — message thread only.

◆ for_each_descendant()

void pulp::state::StateTree::for_each_descendant ( const std::function< void(StateTree &)> & fn)

Visit every descendant (not this node) depth-first, parents before children.

A node's child list is snapshotted right after fn has visited that node, so fn may edit the tree: children it removes from the node being visited are skipped, siblings already snapshotted are still visited even if removed later. NOT thread-safe — message thread only.

◆ from_json()

Ptr pulp::state::StateTree::from_json ( std::string_view json)
static

Deserialize from JSON string.

Returns a freshly created tree, so the call itself is thread-safe; the returned StateTree inherits the same "caller serializes" contract for subsequent access. Thread-safe (returns a new instance owned by the caller).

◆ get()

PropertyValue pulp::state::StateTree::get ( std::string_view name) const

Get a property value (returns monostate if not set).

NOT thread-safe — caller must serialize. std::map::find is a non- atomic read of the tree's internal nodes.

◆ get_bool()

bool pulp::state::StateTree::get_bool ( std::string_view name,
bool default_val = false ) const

Typed getters with defaults.

NOT thread-safe — caller must serialize. Delegate to get().

◆ get_double()

double pulp::state::StateTree::get_double ( std::string_view name,
double default_val = 0.0 ) const

◆ get_int()

int64_t pulp::state::StateTree::get_int ( std::string_view name,
int64_t default_val = 0 ) const

◆ get_string()

std::string pulp::state::StateTree::get_string ( std::string_view name,
std::string_view default_val = "" ) const

◆ has()

bool pulp::state::StateTree::has ( std::string_view name) const

Whether a property exists.

NOT thread-safe — caller must serialize.

◆ in_transaction()

bool pulp::state::StateTree::in_transaction ( ) const

Whether a transaction is open that covers edits made through this node.

NOT thread-safe — caller must serialize.

◆ index_of()

int pulp::state::StateTree::index_of ( const StateTree * node) const

Index of node among this node's children, or -1.

NOT thread-safe — caller must serialize.

◆ insert_child()

void pulp::state::StateTree::insert_child ( int index,
Ptr child )

Insert a child at a specific index (clamped to the valid range).

NOT thread-safe — caller must serialize.

◆ insert_child_checked()

TreeEditResult pulp::state::StateTree::insert_child_checked ( int index,
Ptr child )

Checked insert_child(): an out-of-range index is an error, not clamped.

NOT thread-safe — message thread only.

◆ is_equivalent_to()

bool pulp::state::StateTree::is_equivalent_to ( const StateTree & other) const

Structural equality: same type name, equal properties (exact variant match, so int64 3 differs from double 3.0) and equivalent children in the same order.

Listeners and transaction state are not compared. NOT thread-safe — caller must serialize with mutators of both trees.

◆ move_child()

bool pulp::state::StateTree::move_child ( StateTree & from_parent,
int from_index,
StateTree & to_parent,
int to_index )
static

Move the child at from_index of from_parent to to_index of to_parent.

to_index is the child's final position: for a reorder within one parent it indexes the list after the child is taken out. Same parent: notifies child_moved. Different parent: notifies child_removed, child_added, parent_changed and ancestor_changed. Returns false (and changes nothing) if the move is invalid. NOT thread-safe — message thread only; both parents must be owned by the calling thread.

◆ move_child_checked()

TreeEditResult pulp::state::StateTree::move_child_checked ( StateTree & from_parent,
int from_index,
StateTree & to_parent,
int to_index )
static

Checked move_child().

NOT thread-safe — message thread only.

◆ parent()

StateTree * pulp::state::StateTree::parent ( ) const
inline

Parent (may be null for root).

NOT thread-safe — caller must serialize. The raw back-pointer is rewritten by add_child / insert_child / remove_child / move_child.

◆ property_names()

std::vector< std::string > pulp::state::StateTree::property_names ( ) const

Get all property names (snapshot copy).

NOT thread-safe — caller must serialize. The snapshot is copied while holding no lock; a concurrent set() / remove() can rehash the map during iteration.

◆ remove()

void pulp::state::StateTree::remove ( std::string_view name)

Remove a property.

NOT thread-safe — caller must serialize. Touches properties_ and invokes property-changed listeners with the removed value.

◆ remove_all_children()

void pulp::state::StateTree::remove_all_children ( )

Remove every child.

All children are detached first, then child_removed / parent_changed fire from the last child backwards. NOT thread-safe — caller must serialize.

◆ remove_ancestor_changed_listener()

void pulp::state::StateTree::remove_ancestor_changed_listener ( int id)

◆ remove_checked()

TreeEditResult pulp::state::StateTree::remove_checked ( std::string_view name)

Remove a property; kMissingProperty if it does not exist.

NOT thread-safe — message thread only.

◆ remove_child() [1/2]

void pulp::state::StateTree::remove_child ( int index)

Remove a child by index (notifies child-removed listeners).

NOT thread-safe — caller must serialize.

◆ remove_child() [2/2]

void pulp::state::StateTree::remove_child ( StateTree * child)

Remove a specific child.

NOT thread-safe — caller must serialize.

◆ remove_child_added_listener()

void pulp::state::StateTree::remove_child_added_listener ( int id)

◆ remove_child_checked()

TreeEditResult pulp::state::StateTree::remove_child_checked ( int index)

Checked remove_child(int).

NOT thread-safe — message thread only.

◆ remove_child_moved_listener()

void pulp::state::StateTree::remove_child_moved_listener ( int id)

◆ remove_child_removed_listener()

void pulp::state::StateTree::remove_child_removed_listener ( int id)

◆ remove_listener()

void pulp::state::StateTree::remove_listener ( int id)

◆ remove_parent_changed_listener()

void pulp::state::StateTree::remove_parent_changed_listener ( int id)

◆ set()

void pulp::state::StateTree::set ( std::string_view name,
PropertyValue value )

Set a property value (notifies listeners).

NOT thread-safe — caller must serialize. Touches properties_ and invokes every registered listener inline (or queues the coalesced ones when a transaction is open).

◆ set_checked()

TreeEditResult pulp::state::StateTree::set_checked ( std::string_view name,
PropertyValue value,
const PropertyValidator & validator = {} )

Set a property, returning an error value instead of applying an invalid edit: an empty name, or a value validator refuses.

Intended for edits that originate from scripts or remote tools. NOT thread-safe — message thread only.

◆ to_json()

std::string pulp::state::StateTree::to_json ( ) const

Serialize to JSON string.

NOT thread-safe — caller must serialize. Walks every property and child of the subtree.

◆ type_name()

const std::string & pulp::state::StateTree::type_name ( ) const
inline

Type name (e.g., "preset", "oscillator", "filter").

NOT thread-safe — caller must serialize with any concurrent mutator of the same node. Reading type_name_ while another thread is destroying or reassigning the node races on the std::string body.


The documentation for this class was generated from the following file:
  • /home/runner/work/pulp/pulp/core/state/include/pulp/state/state_tree.hpp