Skip to main content

graphrefly_core/
topology.rs

1//! Core-level topology-change notification primitive.
2//!
3//! The topology sink receives events when nodes are registered,
4//! torn down, or have their deps mutated. This is the substrate
5//! for reactive `describe()` and reactive `observe()` at the
6//! graph layer.
7//!
8//! Topology sinks are NOT nodes — they sit outside the reactive
9//! graph to avoid circularity (registering an observer node
10//! would itself be a topology change).
11
12use std::sync::Arc;
13
14use crate::handle::NodeId;
15
16/// What changed in the topology.
17#[derive(Debug, Clone)]
18pub enum TopologyEvent {
19    /// A new node was registered (state, derived, or dynamic).
20    NodeRegistered(NodeId),
21    /// A node received TEARDOWN (terminal destruction).
22    NodeTornDown(NodeId),
23    /// A node's deps were atomically replaced via `set_deps`.
24    DepsChanged {
25        node: NodeId,
26        old_deps: Vec<NodeId>,
27        new_deps: Vec<NodeId>,
28    },
29}
30
31/// Callback for topology changes. `Send + Sync` for cross-thread Core usage.
32pub type TopologySink = Arc<dyn Fn(&TopologyEvent) + Send + Sync>;
33
34/// RAII handle for a topology subscription. Dropping it unregisters the sink.
35#[must_use = "TopologySubscription holds the subscription; dropping it unregisters the sink"]
36pub struct TopologySubscription {
37    /// Index into `CoreState.topology_sinks`. We use a generational
38    /// approach: each subscription gets a unique id; unsubscribe
39    /// marks the slot as `None`.
40    id: u64,
41    /// Weak ref to `CoreState` — silent no-op if Core is dropped first.
42    state: std::sync::Weak<parking_lot::Mutex<super::node::CoreState>>,
43}
44
45impl Drop for TopologySubscription {
46    fn drop(&mut self) {
47        if let Some(state) = self.state.upgrade() {
48            let mut s = state.lock();
49            s.topology_sinks.remove(&self.id);
50        }
51    }
52}
53
54// Send + Sync compile-time assertion.
55const _: fn() = || {
56    fn assert_send_sync<T: Send + Sync>() {}
57    assert_send_sync::<TopologySubscription>();
58};
59
60impl super::node::Core {
61    /// Subscribe to topology changes. The sink fires synchronously
62    /// from the registration / teardown / `set_deps` call site, under
63    /// no Core lock (the state lock is dropped before firing). Sinks
64    /// MAY re-enter Core (`register_*`, `teardown`, `set_deps`, etc.)
65    /// — the lock-released discipline (Slice A close) makes this safe.
66    ///
67    /// Returns a [`TopologySubscription`] — dropping it unregisters
68    /// the sink.
69    ///
70    /// # Event semantics
71    ///
72    /// - `NodeRegistered(id)` fires from `register_state` /
73    ///   `register_computed`. The Core has finished installing the node
74    ///   record but a Graph-layer namespace name (if any) is NOT yet in
75    ///   place — the sink runs while the caller (`Graph::add`) is still
76    ///   between Core insert and namespace insert. Sinks calling
77    ///   `graph.name_of(id)` from this event will see `None`. Use the
78    ///   Graph-level [`crate::node::Core`]-paired namespace-change hook
79    ///   (graphrefly-graph) for namespace-aware reactivity.
80    /// - `NodeTornDown(id)` fires for the root teardown AND for every
81    ///   meta companion + downstream consumer that auto-cascades. One
82    ///   `Core::teardown(root)` call may produce many events.
83    /// - `DepsChanged { ... }` fires only when `set_deps` actually
84    ///   rewires deps. The idempotent fast-path (deps unchanged as a
85    ///   set) returns without firing.
86    pub fn subscribe_topology(&self, sink: TopologySink) -> TopologySubscription {
87        let mut s = self.lock_state();
88        let id = s.next_topology_id;
89        s.next_topology_id += 1;
90        s.topology_sinks.insert(id, sink);
91        TopologySubscription {
92            id,
93            state: Arc::downgrade(&self.state),
94        }
95    }
96
97    /// Fire topology event to all registered sinks. Called from
98    /// registration, teardown, and `set_deps` sites AFTER the state
99    /// lock is dropped.
100    pub(crate) fn fire_topology_event(&self, event: &TopologyEvent) {
101        let sinks: Vec<TopologySink> = {
102            let s = self.state.lock();
103            s.topology_sinks.values().cloned().collect()
104        };
105        for sink in sinks {
106            sink(event);
107        }
108    }
109}