docs: clarify binding refresh and worker publication boundaries

This commit is contained in:
2026-11-12 19:57:03 +08:00
parent 7f7464ae2b
commit 0cd708f8c7
5 changed files with 35 additions and 3 deletions
+15
View File
@@ -164,6 +164,7 @@ impl Worker {
let Ok(mut current) = namespace.write() else {
return error_response(StatusCode(500), "namespace lock poisoned");
};
// 锁外退役期间可能接受更新提交;过期请求不能再删除当前世代文档中的同编号窗口。
if current.submission != epoch {
return superseded(epoch);
}
@@ -186,12 +187,14 @@ impl Worker {
document.frame.session = 0;
document.frame.focus = None;
for view in document.windows.values_mut() {
// 丢弃的是整个共享 VM,所以剩余每个副窗都撤销交互身份,不能只清主画面或刚关闭的那个窗口。
view.frame.session = 0;
view.frame.focus = None;
}
current.runtime_session = 0;
current.window_commands.clear();
}
// 发布退役后的完整文档;只有 Some 才继续移除视口记录并唤醒 GUI,过期或校验失败都提前返回。
match current.commit(epoch, document) {
Ok(Some(_)) => {}
Ok(None) => return superseded(epoch),
@@ -283,6 +286,7 @@ impl Worker {
// success 可仅有命令而无新帧;错误发布故障条,panic 还要丢弃 VM 与命令世代。
let (document, commands) = match result {
Ok(Ok((document, commands))) => {
// 背景轮询成功有两类结果:有新文档则走下面原子提交,无文档则仅发布可能存在的窗口命令。
if let Some(document) = document {
(document, Some(commands))
} else {
@@ -290,6 +294,7 @@ impl Worker {
let projected = !commands.is_empty();
current.publish_window_commands(id, commands);
drop(current);
// 已释放 Namespace 写锁后才唤醒 GUI;没有命令也没有新帧时保持空闲,不凭轮询次数触发绘制。
if projected {
notify();
}
@@ -320,10 +325,12 @@ impl Worker {
};
// 只有提交成功才发布同世代原生命令和通知 GUI,错误/过期结果不能算一次有效刷新。
let changed = matches!(current.commit(epoch, document), Ok(Some(_)));
// 有效文档和对应命令属于同一会话,提交未接受时不能单独交付命令造成画面/原生窗口状态错配。
if changed && let Some(commands) = commands {
current.publish_window_commands(id, commands);
}
drop(current);
// changed 来自权威 commit 结果,不把失败或被替代的候选当作可见更新通知。
if changed {
notify();
}
@@ -699,6 +706,7 @@ impl Worker {
// 是否需对可见帧加故障条取决于 VM 的实际执行状态,普通编译失败不等于整个域已损坏。
let faulted = session.is_faulted();
if session.has_pending_refresh() {
// 只有正常完成业务输入的结果可保存在 PendingInput;恢复时沿用 result,不再执行一次原始输入。
if let Ok(Ok((result, _))) = result {
return RequestProgress::Pending(PendingRequest::Input(Box::new(PendingInput {
epoch,
@@ -777,6 +785,7 @@ impl Worker {
"input runtime was discarded",
));
};
// 接受序号与真实 VM 会话号都须匹配;同 epoch 中若换过会话,也不能把旧 PendingInput 接到新堆续跑。
if *epoch != input.epoch || session.id != input.id {
return RequestProgress::Complete(superseded(job.submission));
}
@@ -858,6 +867,7 @@ impl Worker {
// rendered 只表示此次交付带新文档,成功执行但页面未变仍可返回 value/session;不因此虚增 revision。
let rendered = result.document.is_some();
if let Some(document) = result.document {
// 可见文档必须先完整验证并被当前 epoch 接受,再发布其窗口命令;不能先回成功响应后补提交。
match current.commit(epoch, document) {
Ok(Some(_)) => {}
Ok(None) => return superseded(job.submission),
@@ -868,6 +878,7 @@ impl Worker {
// 在释放锁前读取这次确定的 revision,响应不再为字符串化借用共享状态。
let revision = current.revision;
drop(current);
// 有新画面或仅有原生命令都要通知;纯值计算没有两者时不强行重绘,响应仍正常返回值。
if rendered || projected {
notify();
}
@@ -887,6 +898,7 @@ impl Worker {
.unwrap_or_else(|| format!("{error:#}").chars().take(1024).collect());
document.frame.fault = Some(fault.clone());
for view in document.windows.values_mut() {
// 这些副窗共享已故障的持久 VM,同步显示同一原因,不能只在发生事件的一个窗口标记故障。
view.frame.fault = Some(fault.clone());
}
// 尽力提交故障条,主响应仍保留原始执行错误;不能因二次故障覆盖真正原因。
@@ -902,6 +914,7 @@ impl Worker {
document.frame.fault =
Some("Runtime discarded after an internal execution failure".into());
for view in document.windows.values_mut() {
// panic 后保留最后完整画面并附统一故障条,下面会撤销运行时资格,不能继续向旧对象派发输入。
view.frame.fault = document.frame.fault.clone();
}
// 输入 panic 保留最近画面但撤销运行时/命令并发布故障条,再将 self.repl 丢弃。
@@ -1113,6 +1126,7 @@ fn build(
Err(_) => error_response(StatusCode(500), "expression execution failed internally"),
};
}
// 一次性构建开始前先拒绝过期 job,节省无权发布的执行;锁外求值结束后仍须再次核验,不能只检查一次。
match namespace.read() {
Ok(current) if current.submission != job.submission => return superseded(job.submission),
Err(_) => return error_response(StatusCode(500), "namespace lock is poisoned"),
@@ -1127,6 +1141,7 @@ fn build(
let Ok(mut current) = namespace.write() else {
return error_response(StatusCode(500), "namespace lock is poisoned");
};
// 这是计算后的第二次检查;期间新提交即使源码相同也拥有新 submission,旧候选不得覆盖它。
if current.submission != job.submission {
return superseded(job.submission);
}
+2 -2
View File
@@ -846,9 +846,9 @@ impl ReplSession {
/// poll_refresh 每量子调用一次,推进稳定循环、一个窗口或一片布局/绘制;true 只表示全轮无新增变化。
fn step_refresh(&mut self, work: &mut RefreshWork) -> Result<bool> {
// 阶段一:新一轮先刷新绑定,再取得稳定窗口遍历顺序。
// 每轮先使属性绑定达到固定点,再取窗口顺序;生命周期或绑定可能改变窗口/页面结构,因此需要多轮确认。
// 每轮先使属性绑定达到固定点,再取窗口顺序;绑定结果应用与生命周期写入可能令依赖再次失效,因此需要多轮确认。
if work.pass_start {
// 绑定/生命周期可能改窗口登记,新的稳定轮必须重新刷新绑定并获取窗口顺序。
// 生命周期可能改窗口登记,绑定结果可能改视觉属性;新的稳定轮必须重新刷新绑定并获取窗口顺序。
self.bridge.refresh_bindings(&mut self.candidate.vm)?;
work.windows = self.bridge.window_ids()?.into();
work.current = None;