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}