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. D246/S2c/D248: single-owner ⇒ no
32/// `Send + Sync` (fires owner-side; the bound was shared-Core-era
33/// legacy).
34pub type TopologySink = Arc<dyn Fn(&TopologyEvent)>;
35
36/// Identifier for a topology subscription (S2b / D225). Returned by
37/// [`super::node::Core::subscribe_topology`]; pass it to
38/// [`super::node::Core::unsubscribe_topology`] to deregister. The
39/// core-level RAII `TopologySubscription` is retired for the same
40/// reason as [`crate::node::SubscriptionId`] (D223: owned relocatable
41/// `Core`, no parameterless-`Drop` reach). Binding-layer RAII wraps
42/// `unsubscribe_topology` where the holder co-owns the `Core`.
43pub type TopologySubscriptionId = u64;
44
45/// Deregister topology sink `id`. Shared body (D225 S2a) for the
46/// synchronous owner-invoked [`super::node::Core::unsubscribe_topology`].
47/// Operates on `&C` directly — it never needed a `Core`.
48pub(crate) fn unsubscribe_topology_sink(core: &crate::node::Core, id: u64) {
49    // D246/S2c: `topology_sinks` lives in the `CoreShared` region
50    // (`St`'s `.shared` field).
51    let mut s = crate::node::St::new(core);
52    s.shared.topology_sinks.remove(&id);
53}
54
55impl super::node::Core {
56    /// Subscribe to topology changes. The sink fires synchronously
57    /// from the registration / teardown / `set_deps` call site, under
58    /// no Core lock (the state lock is dropped before firing). Sinks
59    /// MAY re-enter Core (`register_*`, `teardown`, `set_deps`, etc.)
60    /// — the lock-released discipline (Slice A close) makes this safe.
61    ///
62    /// Returns a [`TopologySubscriptionId`]; pass it to
63    /// [`Self::unsubscribe_topology`] to deregister (S2b / D225: core
64    /// RAII retired — binding-layer RAII wraps `unsubscribe_topology`).
65    ///
66    /// # Event semantics
67    ///
68    /// - `NodeRegistered(id)` fires from `register_state` /
69    ///   `register_computed`. The Core has finished installing the node
70    ///   record but a Graph-layer namespace name (if any) is NOT yet in
71    ///   place — the sink runs while the caller (`Graph::add`) is still
72    ///   between Core insert and namespace insert. Sinks calling
73    ///   `graph.name_of(id)` from this event will see `None`. Use the
74    ///   Graph-level [`crate::node::Core`]-paired namespace-change hook
75    ///   (graphrefly-graph) for namespace-aware reactivity.
76    /// - `NodeTornDown(id)` fires for the root teardown AND for every
77    ///   meta companion + downstream consumer that auto-cascades. One
78    ///   `Core::teardown(root)` call may produce many events.
79    /// - `DepsChanged { ... }` fires only when `set_deps` actually
80    ///   rewires deps. The idempotent fast-path (deps unchanged as a
81    ///   set) returns without firing.
82    pub fn subscribe_topology(&self, sink: TopologySink) -> TopologySubscriptionId {
83        let mut s = self.lock_state();
84        let id = s.shared.next_topology_id;
85        s.shared.next_topology_id += 1;
86        s.shared.topology_sinks.insert(id, sink);
87        id
88    }
89
90    /// Synchronous owner-invoked topology unsubscribe (D225 refined A2).
91    /// Symmetric with `subscribe_topology`; the binding-layer / embedder
92    /// RAII wrapper calls this on `Drop` (it holds the `Core` on its
93    /// affinity worker). Idempotent — removing an absent id is a no-op.
94    pub fn unsubscribe_topology(&self, id: TopologySubscriptionId) {
95        unsubscribe_topology_sink(self, id);
96    }
97
98    /// Fire topology event to all registered sinks. Called from
99    /// registration, teardown, and `set_deps` sites AFTER the state
100    /// lock is dropped.
101    pub(crate) fn fire_topology_event(&self, event: &TopologyEvent) {
102        // Step 2a (D220-EXEC): `topology_sinks` is pure-shared — take
103        // ONLY the `CoreShared` region (no shard lock needed).
104        let sinks: Vec<TopologySink> =
105            self.with_shared(|sh| sh.topology_sinks.values().cloned().collect());
106        for sink in sinks {
107            sink(event);
108        }
109    }
110}