crates\nfarms-plugin\src\context.rs¶
源码路径:
crates\nfarms-plugin\src\context.rs· 620 行
//! 作用域 / 服务容器 / 事件总线。
use std::any::Any;
use std::collections::BTreeMap;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
use crate::plugin::{PluginError, Result};
/// 注册进 [Context] 的服务。
///
/// 用 `Arc<dyn Any + Send + Sync>` 而不是泛型 map:服务表要在**运行时**
/// 按名字查,类型信息只在取出的时候才用得上(见 [`Context::get`])。
pub type Service = Arc<dyn Any + Send + Sync>;
/// 内核事件。
///
/// 用枚举而不是 `&str` + `Box<dyn Any>`:事件的种类是可数的,
/// 枚举能让编译器帮忙查漏,字符串名只会把错误推迟到运行期。
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
/// 插件装载成功
PluginLoaded { name: String },
/// 插件装载失败(依赖缺失不算失败,那种是 Pending)
PluginFailed { name: String, error: String },
/// 插件被卸载
PluginDisposed { name: String },
/// 有一个服务被提供出来。内核靠它重试等待中的插件。
ServiceProvided { name: String },
/// 切到了某一页
PageOpened { id: String },
/// 设置写盘成功
SettingsSaved { path: String },
/// 界面产生的一条日志(插件里想记点什么,又不想直接依赖 nfarms-core::Log 时用它)
Log { source: String, message: String },
}
pub type Listener = Arc<dyn Fn(&Event) + Send + Sync>;
/// [`Context::on`] 的返回值,用来取消订阅。
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub struct ListenerId(u64);
struct Inner {
/// 作用域名。只用于日志和界面展示 —— cordis 里叫 `ctx.name`。
name: String,
/// 父作用域。服务与事件都**向上**可见,不向下。
parent: Option<Arc<Inner>>,
services: RwLock<BTreeMap<&'static str, Service>>,
listeners: RwLock<Vec<(ListenerId, Listener)>>,
/// 服务被 provide 时的回调。内核装在这里,免得 Context 反过来依赖 Kernel。
///
/// 参数是 `&'static str`(服务名本来就是字面量),这样回调能把它直接记进
/// `Vec<&'static str>`,不用先 clone 出一个 String。
///
/// 类型是 `Arc<OnceLock<..>>` 而不是裸 `OnceLock<..>`:fork 出去的子作用域
/// 必须和父作用域**共享同一个单元格**。裸 `OnceLock` 的 `clone` 只是复制当前
/// 内容(`None` clone 出来还是 `None`,且父子是两个独立格子),于是"先 fork、
/// 后由 `Kernel::new` set 钩子"的子作用域永远收不到通知 —— 它 provide 的服务
/// 不会广播给内核,等这个服务的插件就永远停在 Pending。
/// 共享单元格之后,钩子在哪一层被 set,整棵作用域树都看得见
/// (见测试 `provide_hook_is_shared_with_forks_made_before_it`)。
on_provide: Arc<OnceLock<ProvideHook>>,
}
/// 插件之间共享的上下文。
///
/// `Context` 是**句柄**:clone 它不会复制任何东西,两个 clone 指向同一个
/// 作用域。这也是为什么它可以随手塞进闭包、存进结构体。
/// provide 钩子的类型。
///
/// **抽成别名不只是为了短。** 写全了是
/// `Arc<OnceLock<Arc<dyn Fn(&'static str) + Send + Sync>>>` ——
/// 一眼看不出"它就是个回调",而它在签名里出现多次。
/// clippy 的 `type_complexity` 提的就是这件事。
pub type ProvideHook = Arc<dyn Fn(&'static str) + Send + Sync>;
#[derive(Clone)]
pub struct Context {
inner: Arc<Inner>,
}
impl std::fmt::Debug for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Context")
.field("scope", &self.inner.name)
.field("services", &self.service_names())
.finish()
}
}
static NEXT_LISTENER: AtomicU64 = AtomicU64::new(1);
impl Context {
/// 建一个根作用域。
pub fn root(name: impl Into<String>) -> Self {
Self {
inner: Arc::new(Inner {
name: name.into(),
parent: None,
services: RwLock::new(BTreeMap::new()),
listeners: RwLock::new(Vec::new()),
on_provide: Arc::new(OnceLock::new()),
}),
}
}
/// 开一个子作用域。
///
/// 子作用域里 `provide` 的服务**只有它自己和它的后代**看得见 ——
/// 这是插件的隔离手段:两个插件分别 fork 一个作用域,各自注册同名的
/// 服务互不干扰。
///
/// 父子之间只有两条通路:服务/事件**向上可见**,以及 `on_provide` 钩子那个
/// **共享单元格**。所以钩子在 fork 之前还是之后 set 都算数。
///
/// ⚠️ 目前生产代码里没有任何地方调 `fork`(`crates/nfarms-gui` 只走
/// `Kernel::load` / `load_all`),这套隔离能力**只被本文件的单元测试覆盖**,
/// 算是"已经写好的设施"而不是"在跑的功能"。要拿它给真插件做隔离之前,
/// 先补一条跨 crate 的集成测试 —— 否则"两个插件各 fork 一个作用域互不干扰"
/// 只是纸面承诺;在那之前,这里的语义改动没人会替你发现坏掉。
pub fn fork(&self, name: impl Into<String>) -> Self {
Self {
inner: Arc::new(Inner {
name: name.into(),
parent: Some(self.inner.clone()),
services: RwLock::new(BTreeMap::new()),
listeners: RwLock::new(Vec::new()),
// clone 的是 `Arc`,父子指向同一个 `OnceLock`:见 `Inner::on_provide` 的注释
on_provide: self.inner.on_provide.clone(),
}),
}
}
pub fn name(&self) -> &str {
&self.inner.name
}
/// 服务被 provide 时通知内核。只有 [crate::Kernel] 该调它。
///
/// 钩子住在一个**全树共享**的单元格里:只生效一次,后来者被忽略。
/// 内核是在 `Kernel::new()` 里对根作用域调的;某个子作用域哪怕在它之前
/// 就 fork 了,之后照样能收到通知(共享单元格,不是各自一份拷贝)。
pub(crate) fn set_provide_hook(&self, hook: Arc<dyn Fn(&'static str) + Send + Sync>) {
// 已经设过就忽略:内核是单例,重复设只可能是 fork 出去的上下文在捣乱
let _ = self.inner.on_provide.set(hook);
}
/// 注册一个服务。同名会**覆盖**本作用域里的旧值(和 cordis 一样),
/// 但不会影响父作用域。
///
/// 覆盖**不是静默的**:命中旧值时往 stderr 打一条警告(服务名 + 作用域名)。
/// 这里刻意只打警告、不加新的 [`Event`] 变体:事件枚举的消费方
/// (`nfarms-gui` 的 `format_event`)是不带 `_` 分支的穷尽 match,加变体就得
/// 连带改那个 crate,超出本文件能负责的契约;等真的要把服务冲突送进界面时,
/// 再一次性加变体 + 分支。覆盖之后**先拿到旧 `Arc` 的消费者仍然握着旧值**,
/// 警告里会把这点写清楚(类型恰好一致时后挂的赢,先挂的插件不会自动换新的)。
///
/// 名字必须是 `&'static str`:服务表的键就是 `&'static str`
/// (`BTreeMap<&'static str, Service>`),内核靠这一点把服务名直接记进
/// `PluginRecord::provides`,一次分配都不用。查询侧(`get`/`require`/`has`)
/// 收 `&str`,是为了能用运行期拼出来的名字去**查** —— 但**挂**不上去:
/// `format!("page.{id}")` 这种动态名在编译期就被挡住。这是**有意的约束**,
/// 不是漏掉的灵活性;哪天真需要动态名,得把键换成 `String`/`Arc<str>`,
/// 连同内核的 `provides` 和 provide 钩子的签名一起改,别只放宽这一处。
///
/// 返回 `Arc<T>`,省得调用方自己再包一层。
pub fn provide<T: Send + Sync + 'static>(&self, name: &'static str, value: T) -> Arc<T> {
let arc = Arc::new(value);
// 旧值在这里没用(调用方拿的是新值),但 `insert` 会用它决定要不要警告。
let _ = self.insert(name, arc.clone());
arc
}
/// 注册一个**已经包好**的 `Arc`。
///
/// 服务的消费者不止一个时用这个:比如页面注册表既要能 `get` 出来用,
/// 又要能被别的插件持有。
pub fn provide_arc<T: Send + Sync + 'static>(&self, name: &'static str, value: Arc<T>) {
let _ = self.insert(name, value);
}
/// 写进本作用域的服务表,返回被顶掉的旧值(之前没注册过就是 `None`)。
///
/// 返回值不是摆设:生产路径上它决定要不要打下面那条覆盖警告,测试里
/// 它就是"覆盖确实被察觉到了"的证据(`overwriting_a_service_hands_back_the_old_value`)。
fn insert(&self, name: &'static str, value: Service) -> Option<Service> {
// 锁只包住 insert 本身:I/O 不该决定服务表的争用。
let old = self
.inner
.services
.write()
.expect("服务表被毒化")
.insert(name, value);
if old.is_some() {
eprintln!(
"[nfarms-plugin] 服务 `{name}` 在作用域 `{}` 里被覆盖:旧值已被换掉,之前拿到旧 Arc 的消费者还握着旧值。",
self.inner.name
);
}
if let Some(hook) = self.inner.on_provide.get() {
hook(name);
}
old
}
/// 取一个服务。**本作用域找不到就往父作用域找**。
///
/// 类型不对当没找到 —— 名字对上了但类型错了,说明两个插件撞名了,
/// 这时候 `None` 比 panic 好:调用方用 [`Context::require`] 能拿到带名字的错误。
pub fn get<T: Send + Sync + 'static>(&self, name: &str) -> Option<Arc<T>> {
let raw = self.lookup(name)?;
Arc::downcast::<T>(raw).ok()
}
/// 同 [`Context::get`],但拿不到就报错(错误里带上服务名和作用域名)。
pub fn require<T: Send + Sync + 'static>(&self, name: &str) -> Result<Arc<T>> {
match self.lookup(name) {
None => Err(PluginError::new(
self.inner.name.clone(),
format!("依赖的服务 `{name}` 还没有被提供"),
)),
Some(raw) => Arc::downcast::<T>(raw).map_err(|_| {
PluginError::new(
self.inner.name.clone(),
format!("服务 `{name}` 的类型和取用方对不上(多半是两个插件撞名了)"),
)
}),
}
}
fn lookup(&self, name: &str) -> Option<Service> {
// 显式标类型:不写的话 `cur` 会被推成 `Option<&Arc<Inner>>`,
// 而 `parent.as_deref()` 给的是 `Option<&Inner>`,两边对不上。
let mut cur: Option<&Inner> = Some(&self.inner);
while let Some(inner) = cur {
if let Some(v) = inner.services.read().expect("服务表被毒化").get(name) {
return Some(v.clone());
}
cur = inner.parent.as_deref();
}
None
}
/// 这条名字在当前作用域链上有没有(不看类型)。
///
/// 插件加载器就是靠它判断 `inject` 满没满足的。
pub fn has(&self, name: &str) -> bool {
self.lookup(name).is_some()
}
/// 本作用域**自己**注册的服务名(不含继承来的),已排序。
pub fn service_names(&self) -> Vec<&'static str> {
self.inner
.services
.read()
.expect("服务表被毒化")
.keys()
.copied()
.collect()
}
/// 作用域链上所有可见的服务名,去重后排序。
pub fn visible_service_names(&self) -> Vec<&'static str> {
let mut out: Vec<&'static str> = Vec::new();
// 显式标类型:不写的话 `cur` 会被推成 `Option<&Arc<Inner>>`,
// 而 `parent.as_deref()` 给的是 `Option<&Inner>`,两边对不上。
let mut cur: Option<&Inner> = Some(&self.inner);
while let Some(inner) = cur {
out.extend(inner.services.read().expect("服务表被毒化").keys().copied());
cur = inner.parent.as_deref();
}
// 先全收进来再排序去重,而不是每插一个名字就 `Vec::contains` 线性扫一遍
// (那是"作用域层数 × 服务数"的平方级)。结果和旧写法完全一致:
// 同名服务在链上只留一个(子作用域遮蔽父的时候也只算可见的一次)。
out.sort_unstable();
out.dedup();
out
}
/// 订阅事件,返回可以用来取消的 id。
///
/// 事件先发给本作用域,再往上发给父作用域 —— 所以根作用域上的监听器
/// 能看到所有子作用域里发生的事(界面用它来收全局日志)。
pub fn on(&self, listener: impl Fn(&Event) + Send + Sync + 'static) -> ListenerId {
let id = ListenerId(NEXT_LISTENER.fetch_add(1, Ordering::Relaxed));
self.inner
.listeners
.write()
.expect("监听器表被毒化")
.push((id, Arc::new(listener)));
id
}
/// 取消订阅。**沿作用域链往上找**:在根上订阅、从子作用域退订也能退掉
/// (旧实现只翻自己那一层,拿子作用域退父作用域的订阅是静默无效的)。
///
/// 返回**是否真的摘掉了一个监听器**:同一个 id 退第二次、或者传进来一个
/// 不存在/已失效的 id,都返回 `false`。旧签名返回 `()`,调用方没任何办法
/// 知道"这单其实没退成";现在这个 `bool` 就是那条契约,调用方可以直接
/// `assert`/记日志,别再猜。
pub fn off(&self, id: ListenerId) -> bool {
// 显式标类型:不写的话 `cur` 会被推成 `Option<&Arc<Inner>>`,
// 而 `parent.as_deref()` 给的是 `Option<&Inner>`,两边对不上。
let mut cur: Option<&Inner> = Some(&self.inner);
while let Some(inner) = cur {
let removed = {
let mut ls = inner.listeners.write().expect("监听器表被毒化");
let before = ls.len();
ls.retain(|(i, _)| *i != id);
ls.len() != before
};
if removed {
return true;
}
cur = inner.parent.as_deref();
}
false
}
/// 发一个事件。监听器是同步调用的 —— 界面线程里别在监听器里做重活。
///
/// **每个监听器单独 `catch_unwind`**:一个监听器 panic 既不会吃掉同一事件
/// 后面的监听器,也不会顺着 `emit` 传回调用方。旧写法是裸的
/// `for l in chain { l(event) }`,一个 panic 就掀掉整条分发链,而且会一路展开出去:
/// 插件的 `provide` → 钩子里的 `emit` → `plugin.apply` → `Kernel::resolve`。
/// `resolve` 的 `resolving` 标志复位在循环之后、没有任何 RAII 保护,展开后不执行,
/// 之后每次 `resolve()` 都在入口早退,所有插件永远停在 Pending ——
/// 一个插件的坏监听器能把整个宿主的插件装载永久卡死。
///
/// 所以这里**吞掉** panic(而不是分发完再 `resume_unwind`):分发链和内核的
/// 状态机都得活着。代价是 panic 不再向上暴露,因此必须留痕 —— 消息打到 stderr
/// (`Kernel::resolve` 那侧的 RAII guard 是配套的更深一层修法,不在本文件)。
pub fn emit(&self, event: &Event) {
// 显式标类型:不写的话 `cur` 会被推成 `Option<&Arc<Inner>>`,
// 而 `parent.as_deref()` 给的是 `Option<&Inner>`,两边对不上。
let mut cur: Option<&Inner> = Some(&self.inner);
// 先收集再调用:监听器里再 emit(比如插件装载时 provide 又触发
// ServiceProvided)不会撞上正在读的锁。
let mut chain: Vec<Listener> = Vec::new();
while let Some(inner) = cur {
for (_, l) in inner.listeners.read().expect("监听器表被毒化").iter() {
chain.push(l.clone());
}
cur = inner.parent.as_deref();
}
for l in chain {
// AssertUnwindSafe:监听器可以随手抓自己那点状态(插件作者写的东西
// 不该由我们来要求 UnwindSafe),隔离的是"panic 别扩散",不是数据竞争。
if let Err(payload) = catch_unwind(AssertUnwindSafe(|| l(event))) {
eprintln!(
"[nfarms-plugin] 事件监听器 panic,已跳过它并继续分发:{}(事件:{event:?})",
panic_message(payload.as_ref())
);
}
}
}
}
/// 从 `catch_unwind` 的载荷里抠出一句给人看的说明。
///
/// `panic!` 的载荷实际上只有 `&'static str` 和 `String` 两种常见形式,其余
/// (比如 `panic_any`)给个占位说明 —— 这个函数自己绝不能 panic,
/// 否则就在错误处理路径上制造第二个 panic。
fn panic_message(payload: &(dyn Any + Send)) -> String {
if let Some(s) = payload.downcast_ref::<&'static str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"<非字符串 panic 载荷>".to_string()
}
}
/// 一层作用域里的事件监听链长度。**只给单元测试用**:它自己也挂在
/// `#[cfg(test)]` 下,生产构建里这个函数根本不存在,所以别把它当成"自检入口"
/// 写进文档(旧注释就是这么写的,和 `#[cfg(test)]` 自相矛盾)。
#[cfg(test)]
pub(crate) fn listener_count(ctx: &Context) -> usize {
ctx.inner.listeners.read().expect("监听器表被毒化").len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provide_and_get_roundtrip() {
let ctx = Context::root("test");
assert!(ctx.get::<u32>("n").is_none());
ctx.provide("n", 42u32);
assert_eq!(*ctx.get::<u32>("n").unwrap(), 42);
assert!(ctx.has("n"));
// 类型对不上就是 None,不 panic
assert!(ctx.get::<String>("n").is_none());
assert!(ctx.require::<String>("n").is_err());
}
#[test]
fn child_sees_parent_but_not_the_other_way_round() {
let root = Context::root("root");
root.provide("shared", 1u8);
let child = root.fork("child");
child.provide("own", 2u8);
assert!(child.get::<u8>("shared").is_some(), "子作用域要能看见父的");
assert!(root.get::<u8>("own").is_none(), "父作用域不该看见子的");
assert_eq!(child.visible_service_names(), vec!["own", "shared"]);
assert_eq!(root.service_names(), vec!["shared"]);
}
#[test]
fn same_name_in_child_shadows_parent() {
let root = Context::root("root");
root.provide("v", 1u32);
let child = root.fork("child");
child.provide("v", 9u32);
assert_eq!(*root.get::<u32>("v").unwrap(), 1);
assert_eq!(*child.get::<u32>("v").unwrap(), 9);
}
#[test]
fn events_bubble_up_and_off_works() {
let root = Context::root("root");
let hits = Arc::new(std::sync::Mutex::new(Vec::new()));
let h = hits.clone();
let id = root.on(move |e| {
if let Event::Log { message, .. } = e {
h.lock().unwrap().push(message.clone());
}
});
let child = root.fork("child");
child.emit(&Event::Log {
source: "t".into(),
message: "来自子作用域".into(),
});
assert_eq!(hits.lock().unwrap().len(), 1, "子作用域的事件要冒泡到父");
// `off` 现在返回"有没有真退掉":这里顺手把这条契约用起来,
// 免得它变成一个没人看的返回值。
assert!(root.off(id), "在订阅的同一层退订必须成功");
child.emit(&Event::Log {
source: "t".into(),
message: "再来一次".into(),
});
assert_eq!(hits.lock().unwrap().len(), 1, "退订之后不该再收到");
}
#[test]
fn listener_emitting_again_does_not_deadlock() {
// 监听器里再 emit 是常见写法(比如「收到 A 就顺手记一条日志」)。
// 如果事件分发是持锁遍历,这里会直接死锁。
let root = Context::root("root");
let deep = Context::root("deep");
deep.on(|e| {
if let Event::ServiceProvided { .. } = e {
// 什么都不做,只要能在回调里安全地碰 Context 就行
}
});
let counter = Arc::new(AtomicU64::new(0));
let c = counter.clone();
root.on(move |_| {
c.fetch_add(1, Ordering::Relaxed);
});
root.emit(&Event::SettingsSaved {
path: "x".into(),
});
root.emit(&Event::SettingsSaved {
path: "x".into(),
});
assert_eq!(counter.load(Ordering::Relaxed), 2);
assert_eq!(listener_count(&deep), 1);
}
/// 覆盖同名服务**不能再不留痕**:`insert` 必须把旧值交出来,生产路径上
/// 同一个分支会往 stderr 打警告。旧实现把 `insert` 的返回值直接丢掉,
/// 谁也说不清"这个服务是被谁顶掉的"。
#[test]
fn overwriting_a_service_hands_back_the_old_value() {
let ctx = Context::root("test");
let first: Arc<u32> = ctx.provide("svc", 1u32);
let old = ctx
.insert("svc", Arc::new(2u32))
.expect("同名覆盖要把旧值交出来");
// 旧值是以 `Service`(`Arc<dyn Any + Send + Sync>`)交回来的,取内容得 downcast
assert_eq!(old.downcast_ref::<u32>(), Some(&1), "交回的是原来那个值");
let first_as_service: Service = first.clone();
assert!(
Arc::ptr_eq(&old, &first_as_service),
"而且必须是消费者手里那份同一个 Arc,不是拷贝"
);
assert!(
ctx.insert("brand_new", Arc::new(3u32)).is_none(),
"没有旧值的时候不能给假信号"
);
assert_eq!(*ctx.get::<u32>("svc").unwrap(), 2, "表里换成了新值");
assert_eq!(*first, 1, "早就拿到旧 Arc 的消费者还握着旧值");
}
/// 钩子是**共享单元格**:内核在 `Kernel::new` 里 set,而任何在它之前
/// fork 出来的子作用域也必须收到通知。旧实现 clone 裸 `OnceLock`
/// (空 clone 还是空),这些子作用域 provide 的服务永远广播不出去,
/// 等它的插件就永远停在 Pending。
#[test]
fn provide_hook_is_shared_with_forks_made_before_it() {
let root = Context::root("root");
let child = root.fork("child"); // 注意顺序:钩子还没设
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
let s = seen.clone();
root.set_provide_hook(Arc::new(move |name: &'static str| {
s.lock().unwrap().push(name);
}));
child.provide("late", 1u8);
assert_eq!(
*seen.lock().unwrap(),
vec!["late"],
"先 fork 后 set 也要收到:父子共享同一个 OnceLock"
);
// 反方向同样成立:钩子从子作用域设进去,父作用域 provide 也得通知。
// 这正是"共享"和"各一份拷贝"的区别。
let root2 = Context::root("root2");
let child2 = root2.fork("child2");
let seen2 = Arc::new(std::sync::Mutex::new(Vec::new()));
let s2 = seen2.clone();
child2.set_provide_hook(Arc::new(move |name: &'static str| {
s2.lock().unwrap().push(name);
}));
root2.provide("up", 2u8);
assert_eq!(*seen2.lock().unwrap(), vec!["up"]);
}
/// 一个监听器 panic 不能吃掉后面的监听器,也不能顺着 `emit` 传回调用方
/// (调用方是内核的 provide 钩子,展开出去会让 `resolving` 永久卡住)。
#[test]
fn one_panicking_listener_does_not_stop_the_others() {
let root = Context::root("root");
let counter = Arc::new(AtomicU64::new(0));
root.on(|_| panic!("监听器炸了"));
let c = counter.clone();
root.on(move |_| {
c.fetch_add(1, Ordering::Relaxed);
});
let child = root.fork("child");
let c2 = counter.clone();
child.on(move |_| {
c2.fetch_add(1, Ordering::Relaxed);
});
// 分发顺序是"自己的监听器在前,父作用域的在后":
// 子监听器 → 根的 panic 监听器 → 根的正常监听器。
child.emit(&Event::SettingsSaved { path: "x".into() });
assert_eq!(
counter.load(Ordering::Relaxed),
2,
"panic 后面的监听器(而且是在父作用域上的)必须照常收到"
);
// 若 panic 还在往外传,这一行根本执行不到,测试会直接炸掉。
root.emit(&Event::SettingsSaved { path: "x".into() });
assert_eq!(
counter.load(Ordering::Relaxed),
3,
"panic 不能顺着 emit 传回调用方"
);
}
/// `off` 要沿作用域链往上找,并且如实报告有没有退掉。
#[test]
fn off_reaches_the_scope_that_registered_the_listener() {
let root = Context::root("root");
let child = root.fork("child");
let hits = Arc::new(AtomicU64::new(0));
let h = hits.clone();
// 订阅在根上
let id = root.on(move |_| {
h.fetch_add(1, Ordering::Relaxed);
});
assert!(child.off(id), "从子作用域要能退掉父作用域上的订阅");
assert!(!child.off(id), "同一个 id 退第二次要如实返回 false");
assert!(
!child.off(ListenerId(u64::MAX)),
"不存在的 id 要返回 false,不能假装成功"
);
child.emit(&Event::SettingsSaved { path: "x".into() });
assert_eq!(hits.load(Ordering::Relaxed), 0, "退订之后不该再收到");
}
/// 同名服务跨作用域出现时,`visible_service_names` 只能算一次。
#[test]
fn visible_service_names_dedups_names_from_several_scopes() {
let root = Context::root("root");
root.provide("v", 1u32);
let mid = root.fork("mid");
mid.provide("v", 2u32); // 子作用域遮蔽父的同名服务
let leaf = mid.fork("leaf");
leaf.provide("own", 3u8);
assert_eq!(
leaf.visible_service_names(),
vec!["own", "v"],
"链上同名的只算一次(先收集再排序去重,结果和旧的 contains 写法一致)"
);
assert_eq!(mid.service_names(), vec!["v"], "本作用域自己的表不受影响");
}
}