1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! Global cell registry for the inspector feature.
//!
//! Tracks all live cells and their ownership relationships using lock-free data structures.
use std::sync::{OnceLock, Weak};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::traits::DepNode;
const MAX_VALUE_LEN: usize = 4096;
/// Snapshot of a single cell's state, suitable for serialization and transmission.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CellSnapshot {
pub id: Uuid,
pub name: Option<String>,
pub display_name: String,
pub subscriber_count: usize,
pub owned_count: usize,
pub dep_ids: Vec<Uuid>,
/// The id of the cell that owns this cell (via `Cell::own()`), if any.
pub owner_id: Option<Uuid>,
/// Debug-formatted current value.
#[serde(default)]
pub value: Option<String>,
/// Source location where this cell was created (file:line:col).
#[serde(default)]
pub caller: Option<String>,
}
/// Global registry of all live hyphae cells.
///
/// Uses `DashMap` for lock-free concurrent access. Cells register themselves on creation
/// and deregister on drop. Ownership relationships (from `Cell::own()`) are tracked
/// separately so the inspector can identify root cells (those with no owner).
pub struct CellRegistry {
/// All live cells, stored as weak references so the registry doesn't prevent GC.
cells: DashMap<Uuid, Weak<dyn DepNode>>,
/// Ownership: child_id → parent_id. Populated when `Cell::own(guard)` is called.
ownership: DashMap<Uuid, Uuid>,
}
impl CellRegistry {
fn new() -> Self {
Self {
cells: DashMap::new(),
ownership: DashMap::new(),
}
}
/// Register a cell in the registry.
pub fn register(&self, id: Uuid, weak: Weak<dyn DepNode>) {
self.cells.insert(id, weak);
}
/// Deregister a cell from the registry.
pub fn deregister(&self, id: &Uuid) {
self.cells.remove(id);
self.ownership.remove(id);
}
/// Record that `parent_id` owns `child_id` (the child's subscription source).
pub fn mark_owned(&self, child_id: Uuid, parent_id: Uuid) {
self.ownership.insert(child_id, parent_id);
}
/// Remove ownership tracking for a child cell.
pub fn unmark_owned(&self, child_id: Uuid) {
self.ownership.remove(&child_id);
}
/// Take a snapshot of all live cells. Automatically garbage-collects stale entries.
pub fn snapshot(&self) -> Vec<CellSnapshot> {
let mut snapshots = Vec::new();
let mut stale = Vec::new();
for entry in self.cells.iter() {
let id = *entry.key();
match entry.value().upgrade() {
Some(node) => {
let dep_ids: Vec<Uuid> = node.deps().iter().map(|d| d.id()).collect();
let owner_id = self.ownership.get(&id).map(|e| *e.value());
let value = node.value_debug().map(|mut s| {
if s.len() > MAX_VALUE_LEN {
s.truncate(MAX_VALUE_LEN);
s.push('…');
}
s
});
let caller = node
.caller()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()));
snapshots.push(CellSnapshot {
id,
name: node.name(),
display_name: node.display_name(),
subscriber_count: node.subscriber_count(),
owned_count: node.owned_count(),
dep_ids,
owner_id,
value,
caller,
});
}
None => {
stale.push(id);
}
}
}
// GC stale entries
for id in stale {
self.cells.remove(&id);
self.ownership.remove(&id);
}
snapshots
}
/// Remove all entries where the weak reference can no longer be upgraded.
pub fn gc(&self) {
let stale: Vec<Uuid> = self
.cells
.iter()
.filter(|e| e.value().upgrade().is_none())
.map(|e| *e.key())
.collect();
for id in stale {
self.cells.remove(&id);
self.ownership.remove(&id);
}
}
/// Number of tracked cells (including potentially stale entries).
pub fn len(&self) -> usize {
self.cells.len()
}
/// Whether the registry is empty.
pub fn is_empty(&self) -> bool {
self.cells.is_empty()
}
}
static REGISTRY: OnceLock<CellRegistry> = OnceLock::new();
/// Get the global cell registry, initializing it on first access.
pub fn registry() -> &'static CellRegistry {
REGISTRY.get_or_init(CellRegistry::new)
}